1
0
Fork 0

feat(v4): add new UI (#2555)

* feat: add new footer ui

* feat: add new search box ui

* feat: add searchbox actions

* feat: add start screen

* feat: add no results screen

* feat: add new card ui

* feat: add new icons

* feat: add new key press class

* fix: cypress tests

* fix: circleci

* fix: circleci

* fix: close aria label

* fix: circleci

* fix: remove unused classes

* fix: circleci

* feat: update version

* feat: add new dark mode ui

* fix: colors

* fix: design review

* fix: hit title length

* fix: improve accessibility

* fix: ci

* chore: increase bundle size threshold

* fix: css
This commit is contained in:
Vasco Bettencourt 2025-04-14 22:34:23 +03:00 committed by GitHub
parent ec351710b1
commit 6a160aecf6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 434 additions and 302 deletions

View file

@ -2,7 +2,7 @@
"files": [
{
"path": "packages/docsearch-css/dist/style.css",
"maxSize": "3.25 kB"
"maxSize": "3.50 kB"
},
{
"path": "packages/docsearch-react/dist/umd/index.js",

View file

@ -12,7 +12,7 @@ describe('Start', () => {
// check that the scrollbar offset is compensated
cy.get('body').should('have.css', 'overflow', 'hidden');
cy.get('body').should('have.css', 'margin-right', '15px');
cy.get('.DocSearch-Modal').should('be.visible');
});
it('Open modal with key shortcut on Windows/Linux', () => {
@ -53,7 +53,7 @@ describe('End', () => {
});
it('Close modal by clicking outside its container', () => {
cy.get('.DocSearch-Container').click();
cy.get('body').click(0, 0);
cy.modalIsNotVisible();
});
@ -81,7 +81,7 @@ describe('Search', () => {
it('Query can be cleared', () => {
cy.typeQueryMatching();
cy.get('.DocSearch-Reset').click();
cy.get('.DocSearch-Clear').click();
cy.get('.DocSearch-Hits').should('not.exist');
cy.contains('No recent searches').should('be.visible');
});
@ -101,7 +101,9 @@ describe('Search', () => {
const currentURL = cy.url();
cy.typeQueryMatching();
cy.get('.DocSearch-Hits #docsearch-hits0-item-1 > a').click({ force: true });
cy.get('.DocSearch-Hits #docsearch-hits0-item-1 > a').click({
force: true,
});
cy.on('url:changed', (newUrl) => {
expect(newUrl).not.equal(currentURL);
});

View file

@ -1,16 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DocSearch v4 - React</title>
</head>
<head>
<meta charset="UTF-8" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DocSearch v3 - React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -6,7 +6,7 @@ import '@docsearch/css/dist/style.css';
function App(): React.JSX.Element {
return (
<div>
<h1>DocSearch v3 - React - 18</h1>
<h1>DocSearch v4 - React - 18</h1>
<DocSearch indexName="vuejs" appId="ML0LEBN7FQ" apiKey="21cf9df0734770a2448a9da64a700c22" insights={true} />
</div>
);

View file

@ -1,16 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DocSearch v4 - React</title>
</head>
<head>
<meta charset="UTF-8" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DocSearch v3 - React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -6,7 +6,7 @@ import '@docsearch/css/dist/style.css';
function App(): JSX.Element {
return (
<div>
<h1>DocSearch v3 - React</h1>
<h1>DocSearch v4 - React</h1>
<DocSearch indexName="vuejs" appId="ML0LEBN7FQ" apiKey="21cf9df0734770a2448a9da64a700c22" insights={true} />
</div>
);

View file

@ -1,17 +1,23 @@
/* Variables */
:root {
--docsearch-primary-color: rgb(84, 104, 255);
--docsearch-text-color: rgb(28, 30, 33);
--docsearch-primary-color: rgb(0, 61, 255);
--docsearch-subtle-color: rgb(214, 214, 231);
--docsearch-text-color: rgba(35, 38, 59, 1);
--docsearch-secondary-text-color: rgba(90, 94, 154, 1);
--docsearch-background-color: rgb(245, 245, 250);
--docsearch-spacing: 12px;
--docsearch-icon-stroke-width: 1.4;
--docsearch-highlight-color: var(--docsearch-primary-color);
--docsearch-muted-color: rgb(150, 159, 175);
--docsearch-focus-color: rgb(0, 95, 204);
--docsearch-highlight-color: rgb(0, 61, 255);
--docsearch-muted-color: rgba(150, 152, 195, 1);
--docsearch-muted-color-darker: rgba(120, 122, 165, 0.25);
--docsearch-icon-color: rgba(90, 94, 154, 1);
--docsearch-container-background: rgba(101, 108, 133, 0.8);
--docsearch-logo-color: rgba(84, 104, 255);
--docsearch-logo-color: rgba(0, 61, 255, 1);
/* modal */
--docsearch-modal-width: 560px;
--docsearch-modal-width: 800px;
--docsearch-modal-height: 600px;
--docsearch-modal-background: rgb(245, 246, 247);
--docsearch-modal-shadow: inset 1px 1px 0 0 rgba(255, 255, 255, 0.5),
@ -19,30 +25,24 @@
/* searchbox */
--docsearch-searchbox-height: 56px;
--docsearch-searchbox-background: rgb(235, 237, 240);
--docsearch-searchbox-background: #fff;
--docsearch-searchbox-focus-background: #fff;
--docsearch-searchbox-shadow: inset 0 0 0 2px var(--docsearch-primary-color);
--docsearch-actions-width: 99px;
--docsearch-actions-height: 44px;
/* hit */
--docsearch-hit-height: 56px;
--docsearch-hit-color: rgb(68, 73, 80);
--docsearch-hit-active-color: #fff;
--docsearch-hit-highlight-color: rgba(0, 61, 255, 0.1);
--docsearch-hit-background: #fff;
--docsearch-hit-shadow: 0 1px 3px 0 rgb(212, 217, 225);
/* key */
--docsearch-key-gradient: linear-gradient(
-225deg,
rgb(213, 219, 228) 0%,
rgb(248, 248, 248) 100%
);
--docsearch-key-shadow: inset 0 -2px 0 0 rgb(205, 205, 230),
inset 0 0 1px 1px #fff, 0 1px 2px 1px rgba(30, 35, 90, 0.4);
--docsearch-key-pressed-shadow: inset 0 -2px 0 0 #cdcde6,
inset 0 0 1px 1px #fff, 0 1px 1px 0 rgba(30, 35, 90, 0.4);
--docsearch-key-background: rgb(245, 245, 250);
--docsearch-key-color: rgba(90, 94, 154, 1);
--docsearch-key-pressed-shadow: inset 0 2px 4px rgba(120, 122, 165, 0.25);
/* footer */
--docsearch-footer-height: 44px;
--docsearch-footer-height: 52px;
--docsearch-footer-background: #fff;
--docsearch-footer-shadow: 0 -1px 0 0 rgb(224, 227, 232),
0 -3px 6px 0 rgba(69, 98, 155, 0.12);
@ -51,7 +51,13 @@
/* Darkmode */
html[data-theme='dark'] {
--docsearch-text-color: rgb(245, 246, 247);
--docsearch-text-color: rgba(196, 199, 220, 1);
--docsearch-secondary-text-color: rgba(182, 183, 213, 1);
--docsearch-subtle-color: rgba(33, 33, 57, 1);
--docsearch-highlight-color: rgba(69, 122, 255, 1);
--docsearch-focus-color: rgb(154, 200, 255);
--docsearch-background-color: rgba(54, 57, 90, 1);
--docsearch-icon-color: rgba(182, 183, 213, 1);
--docsearch-container-background: rgba(9, 10, 17, 0.8);
--docsearch-modal-background: rgb(21, 23, 42);
--docsearch-modal-shadow: inset 1px 1px 0 0 rgb(44, 46, 64),
@ -61,16 +67,10 @@ html[data-theme='dark'] {
--docsearch-hit-color: rgb(190, 195, 201);
--docsearch-hit-shadow: none;
--docsearch-hit-background: rgb(9, 10, 17);
--docsearch-key-gradient: linear-gradient(
-26.5deg,
rgb(86, 88, 114) 0%,
rgb(49, 53, 91) 100%
);
--docsearch-key-shadow: inset 0 -2px 0 0 rgb(40, 45, 85),
inset 0 0 1px 1px rgb(81, 87, 125), 0 2px 2px 0 rgba(3, 4, 9, 0.3);
--docsearch-key-pressed-shadow: inset 0 -2px 0 0 #282d55,
inset 0 0 1px 1px #51577d, 0 1px 1px 0 #0304094d;
--docsearch-footer-background: rgb(30, 33, 54);
--docsearch-key-background: rgba(54, 57, 90, 1);
--docsearch-key-color: rgba(182, 183, 213, 1);
--docsearch-key-pressed-shadow: inset 0 2px 4px rgba(12, 13, 20, 0.4);
--docsearch-footer-background: rgba(13, 13, 13, 1);
--docsearch-footer-shadow: inset 0 1px 0 0 rgba(73, 76, 106, 0.5),
0 -4px 8px 0 rgba(0, 0, 0, 0.2);
--docsearch-logo-color: rgb(255, 255, 255);

View file

@ -1,12 +1,13 @@
.DocSearch-Button {
align-items: center;
background: var(--docsearch-searchbox-background);
border: 0;
border-radius: 40px;
border: 1px solid var(--docsearch-subtle-color);
border-radius: 4px;
background-color: var(--docsearch-searchbox-background);
color: var(--docsearch-muted-color);
cursor: pointer;
display: flex;
font-weight: 500;
font-weight: 300;
width: 286px;
height: 36px;
justify-content: space-between;
margin: 0 0 0 16px;
@ -14,31 +15,27 @@
user-select: none;
}
.DocSearch-Button:hover,
.DocSearch-Button:active,
.DocSearch-Button:focus {
background: var(--docsearch-searchbox-focus-background);
box-shadow: var(--docsearch-searchbox-shadow);
color: var(--docsearch-text-color);
outline: none;
}
.DocSearch-Button-Container {
height: 100%;
align-items: center;
display: flex;
}
.DocSearch-Button-Container svg {
color: var(--docsearch-highlight-color);
}
.DocSearch-Search-Icon {
color: var(--docsearch-highlight-color);
stroke-width: 1.6;
}
.DocSearch-Button .DocSearch-Search-Icon {
color: var(--docsearch-text-color);
}
.DocSearch-Button-Placeholder {
font-size: 1rem;
padding: 0 12px 0 6px;
font-size: 1.25rem;
font-weight: 300;
padding: 0 12px 0 8px;
display: inline-block;
line-height: normal;
}
.DocSearch-Button-Keys {
@ -48,36 +45,42 @@
.DocSearch-Button-Key {
align-items: center;
background: var(--docsearch-key-gradient);
border-radius: 3px;
box-shadow: var(--docsearch-key-shadow);
color: var(--docsearch-muted-color);
background: var(--docsearch-key-background);
border-radius: 2px;
color: var(--docsearch-key-color);
box-shadow: none !important;
display: flex;
height: 18px;
height: 24px;
font-size: 20px;
justify-content: center;
margin-right: 0.4em;
position: relative;
padding: 0 0 2px;
border: 0;
top: -1px;
width: 20px;
width: 24px;
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 100ms;
}
@media (prefers-reduced-motion) {
.DocSearch-Button-Key:first-child {
margin-right: 0.4em;
}
@media (prefers-reduced-motion) {
.DocSearch-Button-Key {
transition: none;
}
}
.DocSearch-Button-Key--pressed {
transform: translate3d(0, 1px, 0);
box-shadow: var(--docsearch-key-pressed-shadow);
transform: translateY(1px);
box-shadow: var(--docsearch-key-pressed-shadow) !important;
}
@media (max-width: 768px) {
.DocSearch-Button {
width: auto;
}
.DocSearch-Button-Keys,
.DocSearch-Button-Placeholder {
display: none;

View file

@ -42,7 +42,7 @@
.DocSearch-Modal {
background: var(--docsearch-modal-background);
border-radius: 6px;
border-radius: 4px;
box-shadow: var(--docsearch-modal-shadow);
flex-direction: column;
margin: 60px auto auto;
@ -54,18 +54,21 @@
.DocSearch-SearchBar {
display: flex;
padding: var(--docsearch-spacing) var(--docsearch-spacing) 0;
}
.DocSearch-SearchBar-Magnifier {
color: var(--docsearch-highlight-color);
}
.DocSearch-Form {
align-items: center;
background: var(--docsearch-searchbox-focus-background);
border-radius: 4px;
box-shadow: var(--docsearch-searchbox-shadow);
border-bottom: 1px solid var(--docsearch-subtle-color);
border-radius: 4px 4px 0 0;
display: flex;
height: var(--docsearch-searchbox-height);
margin: 0;
padding: 0 var(--docsearch-spacing);
padding: var(--docsearch-spacing) 16px;
position: relative;
width: 100%;
}
@ -78,6 +81,7 @@
flex: 1;
font: inherit;
font-size: 1.2em;
font-weight: 300;
height: 100%;
outline: none;
padding: 0 0 0 8px;
@ -96,17 +100,32 @@
display: none;
}
.DocSearch-Actions {
width: var(--docsearch-actions-width);
height: var(--docsearch-actions-height);
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 2px;
gap: 16px;
}
.DocSearch-Divider {
height: 24px;
border-left: 1px solid var(--docsearch-subtle-color);
}
.DocSearch-LoadingIndicator,
.DocSearch-MagnifierLabel,
.DocSearch-Reset {
.DocSearch-Close {
margin: 0;
padding: 0;
}
.DocSearch-MagnifierLabel,
.DocSearch-Reset {
.DocSearch-Close {
align-items: center;
color: var(--docsearch-highlight-color);
color: var(--docsearch-highlight-color) !important;
display: flex;
justify-content: center;
}
@ -127,7 +146,7 @@
}
@media screen and (prefers-reduced-motion: reduce) {
.DocSearch-Reset {
.DocSearch-Close {
animation: none;
appearance: none;
background: none;
@ -140,7 +159,7 @@
}
}
.DocSearch-Reset {
.DocSearch-Close {
animation: fade-in 0.1s ease-in forwards;
appearance: none;
background: none;
@ -153,28 +172,51 @@
stroke-width: var(--docsearch-icon-stroke-width);
}
.DocSearch-Reset[hidden] {
.DocSearch-Close[hidden] {
display: none;
}
.DocSearch-Reset:hover {
.DocSearch-Close:hover {
color: var(--docsearch-highlight-color);
}
.DocSearch-LoadingIndicator svg,
.DocSearch-MagnifierLabel svg {
color: var(--docsearch-highlight-color) !important;
height: 24px;
width: 24px;
}
.DocSearch-Cancel {
display: none;
.DocSearch-Clear {
appearance: none;
background: none;
border: 0;
color: var(--docsearch-highlight-color);
cursor: pointer;
flex: none;
font: inherit;
font-size: 0.9em;
font-weight: 300;
height: 28px;
margin-left: var(--docsearch-spacing);
outline: none;
overflow: hidden;
padding: 0;
user-select: none;
white-space: nowrap;
}
.DocSearch-Close:focus-visible,
.DocSearch-Clear:focus-visible {
outline: 2px solid var(--docsearch-focus-color);
outline-offset: 1px;
border-radius: 4px;
}
/* Modal Dropdown */
.DocSearch-Dropdown {
max-height: calc(
height: calc(
var(--docsearch-modal-height) - var(--docsearch-searchbox-height) -
var(--docsearch-spacing) - var(--docsearch-footer-height)
);
@ -208,20 +250,40 @@
}
.DocSearch-Label {
color: var(--docsearch-muted-color);
font-size: 0.75em;
color: var(--docsearch-secondary-text-color);
font-size: 0.875em;
line-height: 1.6em;
font-weight: 300;
}
.DocSearch-NoResults-Help {
color: var(--docsearch-secondary-text-color);
font-size: 0.8em;
margin: 0;
user-select: none;
line-height: 1.5em;
font-weight: 300;
}
.DocSearch-Help {
color: var(--docsearch-muted-color);
font-size: 0.9em;
color: var(--docsearch-secondary-text-color);
font-size: 0.8em;
margin: 0;
user-select: none;
line-height: 1.5em;
font-weight: 300;
}
.DocSearch-Title {
font-size: 1.2em;
color: var(--docsearch-text-color);
vertical-align: middle;
font-size: 1.1em;
line-height: 0.5em;
font-weight: 300;
}
.DocSearch-Title strong {
font-weight: 500;
}
.DocSearch-Logo a {
@ -293,7 +355,6 @@
.DocSearch-Hit a {
background: var(--docsearch-hit-background);
border-radius: 4px;
box-shadow: var(--docsearch-hit-shadow);
display: block;
padding-left: var(--docsearch-spacing);
width: 100%;
@ -301,12 +362,12 @@
.DocSearch-Hit-source {
background: var(--docsearch-modal-background);
color: var(--docsearch-highlight-color);
font-size: 0.85em;
color: var(--docsearch-text-color);
font-size: 0.9em;
font-weight: 600;
line-height: 32px;
margin: 0 -4px;
padding: 8px 4px 0;
padding: 8px 4px 4px;
position: sticky;
top: 0;
z-index: 10;
@ -321,16 +382,17 @@
}
.DocSearch-Hit[aria-selected='true'] a {
background-color: var(--docsearch-highlight-color);
background-color: var(--docsearch-hit-highlight-color) !important;
}
.DocSearch-Hit[aria-selected='true'] mark {
.DocSearch-Hit mark {
color: var(--docsearch-highlight-color);
text-decoration: underline;
}
.DocSearch-Hit-Container {
align-items: center;
color: var(--docsearch-hit-color);
color: var(--docsearch-text-color);
display: flex;
flex-direction: row;
height: var(--docsearch-hit-height);
@ -338,7 +400,7 @@
}
.DocSearch-Hit-icon {
color: var(--docsearch-muted-color);
color: var(--docsearch-secondary-text-color);
height: 20px;
stroke-width: var(--docsearch-icon-stroke-width);
width: 20px;
@ -395,13 +457,6 @@ svg.DocSearch-Hit-Select-Icon {
transition: background-color 0.1s ease-in;
}
@media screen and (prefers-reduced-motion: reduce) {
.DocSearch-Hit-action-button:hover,
.DocSearch-Hit-action-button:focus {
transition: none;
}
}
.DocSearch-Hit-action-button:hover path,
.DocSearch-Hit-action-button:focus path {
fill: #fff;
@ -420,6 +475,7 @@ svg.DocSearch-Hit-Select-Icon {
text-overflow: ellipsis;
white-space: nowrap;
width: 80%;
gap: 4px;
}
.DocSearch-Hit-title {
@ -427,18 +483,20 @@ svg.DocSearch-Hit-Select-Icon {
}
.DocSearch-Hit-path {
color: var(--docsearch-muted-color);
color: var(--docsearch-secondary-text-color);
font-size: 0.75em;
}
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-title,
.DocSearch-Hit[aria-selected='true'] mark,
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-text,
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-path,
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-icon,
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-action,
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-Tree {
color: var(--docsearch-hit-active-color) !important;
color: var(--docsearch-text-color) !important;
}
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-action,
.DocSearch-Hit[aria-selected='true'] mark,
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-icon {
color: var(--docsearch-highlight-color) !important;
}
@media screen and (prefers-reduced-motion: reduce) {
@ -454,32 +512,64 @@ svg.DocSearch-Hit-Select-Icon {
.DocSearch-NoResults,
.DocSearch-StartScreen,
.DocSearch-ErrorScreen {
font-size: 0.9em;
font-size: 1.25em;
font-weight: 400;
margin: 0 auto;
padding: 36px 0;
color: var(--docsearch-secondary-text-color);
text-align: center;
width: 80%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.DocSearch-NoResults,
.DocSearch-ErrorScreen {
height: 80%;
}
.DocSearch-StartScreen {
height: 100%;
}
.DocSearch-NoResults {
gap: 24px;
}
.DocSearch-StartScreen,
.DocSearch-ErrorScreen {
gap: 24px;
}
.DocSearch-StartScreen-Icon {
width: 64px;
height: 64px;
stroke: var(--docsearch-icon-color);
}
.DocSearch-Screen-Icon {
color: var(--docsearch-muted-color);
padding-bottom: 12px;
color: var(--docsearch-icon-color);
}
.DocSearch-NoResults-Prefill-List {
display: inline-block;
padding-bottom: 24px;
text-align: left;
display: flex;
flex-direction: column;
gap: 8px;
text-align: center;
}
.DocSearch-NoResults-Prefill-List ul {
display: inline-block;
display: flex;
flex-direction: column;
gap: 8px;
height: 40px;
padding: 8px 0 0;
}
.DocSearch-NoResults-Prefill-List li {
list-style-position: inside;
list-style-type: '» ';
display: flex;
align-items: center;
}
.DocSearch-Prefill {
@ -489,10 +579,13 @@ svg.DocSearch-Hit-Select-Icon {
border-radius: 1em;
color: var(--docsearch-highlight-color);
cursor: pointer;
display: inline-block;
font-size: 1em;
font-weight: 700;
padding: 0;
display: inline-flex;
align-items: center;
gap: 4px;
height: 40px;
font-size: 0.8em;
font-weight: 300;
padding: 0 4px;
}
.DocSearch-Prefill:hover,
@ -506,8 +599,8 @@ svg.DocSearch-Hit-Select-Icon {
.DocSearch-Footer {
align-items: center;
background: var(--docsearch-footer-background);
border-radius: 0 0 8px 8px;
box-shadow: var(--docsearch-footer-shadow);
border-top: 1px solid var(--docsearch-subtle-color);
border-radius: 0 0 4px 4px;
display: flex;
flex-direction: row-reverse;
flex-shrink: 0;
@ -526,6 +619,7 @@ svg.DocSearch-Hit-Select-Icon {
list-style: none;
margin: 0;
padding: 0;
gap: 16px;
}
.DocSearch-Commands li {
@ -533,23 +627,31 @@ svg.DocSearch-Hit-Select-Icon {
display: flex;
}
.DocSearch-Commands li:not(:last-of-type) {
margin-right: 0.8em;
}
.DocSearch-Commands-Key {
align-items: center;
background: var(--docsearch-key-gradient);
border-radius: 2px;
box-shadow: var(--docsearch-key-shadow);
background-color: var(--docsearch-background-color);
display: flex;
height: 18px;
height: 24px;
justify-content: center;
margin-right: 0.4em;
padding: 0 0 1px;
color: var(--docsearch-muted-color);
margin-right: 4px;
color: var(--docsearch-icon-color);
border: 0;
width: 20px;
width: 24px;
box-shadow: none !important;
}
.DocSearch-Commands-Key:last-of-type {
margin-right: 8px;
}
.DocSearch-Escape-Key {
font-weight: 300;
font-size: 10px;
line-height: 16px;
letter-spacing: normal;
text-align: center;
text-transform: uppercase;
}
/* Hide element accessibly, so that it is still accessible to
@ -568,7 +670,7 @@ assistive tech users */
@media (max-width: 768px) {
:root {
--docsearch-spacing: 10px;
--docsearch-footer-height: 40px;
--docsearch-footer-height: 48px;
}
.DocSearch-Dropdown {
@ -612,25 +714,6 @@ assistive tech users */
);
}
.DocSearch-Cancel {
appearance: none;
background: none;
border: 0;
color: var(--docsearch-highlight-color);
cursor: pointer;
display: inline-block;
flex: none;
font: inherit;
font-size: 1em;
font-weight: 500;
margin-left: var(--docsearch-spacing);
outline: none;
overflow: hidden;
padding: 0;
user-select: none;
white-space: nowrap;
}
.DocSearch-Commands {
display: none;
}

View file

@ -1,7 +1,7 @@
import React, { type JSX } from 'react';
type AlgoliaLogoTranslations = Partial<{
searchByText: string;
poweredByText: string;
}>;
type AlgoliaLogoProps = {
@ -9,7 +9,7 @@ type AlgoliaLogoProps = {
};
export function AlgoliaLogo({ translations = {} }: AlgoliaLogoProps): JSX.Element {
const { searchByText = 'Search by' } = translations;
const { poweredByText = 'Powered by' } = translations;
return (
<a
@ -17,10 +17,10 @@ export function AlgoliaLogo({ translations = {} }: AlgoliaLogoProps): JSX.Elemen
target="_blank"
rel="noopener noreferrer"
>
<span className="DocSearch-Label">{searchByText}</span>
<span className="DocSearch-Label">{poweredByText}</span>
<svg
width="77"
height="19"
height="24"
aria-label="Algolia"
role="img"
id="Layer_1"

View file

@ -1,6 +1,7 @@
import React, { type JSX, useEffect, useState } from 'react';
import { ControlKeyIcon } from './icons/ControlKeyIcon';
import { MetaKeyIcon } from './icons/MetaKeyIcon';
import { SearchIcon } from './icons/SearchIcon';
export type ButtonTranslations = Partial<{
@ -35,7 +36,8 @@ export const DocSearchButton = React.forwardRef<HTMLButtonElement, DocSearchButt
key === ACTION_KEY_DEFAULT
? // eslint-disable-next-line react/jsx-key -- false flag
([ACTION_KEY_DEFAULT, 'Ctrl', <ControlKeyIcon />] as const)
: (['Meta', 'Command', key] as const);
: // eslint-disable-next-line react/jsx-key
(['Meta', 'Command', <MetaKeyIcon />] as const);
return (
<button

View file

@ -66,7 +66,7 @@ export function DocSearchModal({
const formElementRef = React.useRef<HTMLDivElement | null>(null);
const dropdownRef = React.useRef<HTMLDivElement | null>(null);
const inputRef = React.useRef<HTMLInputElement | null>(null);
const snippetLength = React.useRef<number>(10);
const snippetLength = React.useRef<number>(15);
const initialQueryFromSelection = React.useRef(
typeof window !== 'undefined' ? window.getSelection()!.toString().slice(0, MAX_QUERY_SIZE) : '',
).current;

View file

@ -10,7 +10,7 @@ export type FooterTranslations = Partial<{
navigateDownKeyAriaLabel: string;
closeText: string;
closeKeyAriaLabel: string;
searchByText: string;
poweredByText: string;
}>;
type FooterProps = Partial<{
@ -24,8 +24,8 @@ interface CommandIconProps {
function CommandIcon(props: CommandIconProps): JSX.Element {
return (
<svg width="15" height="15" aria-label={props.ariaLabel} role="img">
<g fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.2">
<svg width="20" height="20" aria-label={props.ariaLabel} viewBox="0 0 24 24" role="img">
<g fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.4">
{props.children}
</g>
</svg>
@ -34,50 +34,53 @@ function CommandIcon(props: CommandIconProps): JSX.Element {
export function Footer({ translations = {} }: FooterProps): JSX.Element {
const {
selectText = 'to select',
selectText = 'Select',
selectKeyAriaLabel = 'Enter key',
navigateText = 'to navigate',
navigateText = 'Navigate',
navigateUpKeyAriaLabel = 'Arrow up',
navigateDownKeyAriaLabel = 'Arrow down',
closeText = 'to close',
closeText = 'Close',
closeKeyAriaLabel = 'Escape key',
searchByText = 'Search by',
poweredByText = 'Powered by',
} = translations;
return (
<>
<div className="DocSearch-Logo">
<AlgoliaLogo translations={{ searchByText }} />
<AlgoliaLogo translations={{ poweredByText }} />
</div>
<ul className="DocSearch-Commands">
<li>
<kbd className="DocSearch-Commands-Key">
<CommandIcon ariaLabel={selectKeyAriaLabel}>
<path d="M12 3.53088v3c0 1-1 2-2 2H4M7 11.53088l-3-3 3-3" />
</CommandIcon>
</kbd>
<span className="DocSearch-Label">{selectText}</span>
</li>
<li>
<kbd className="DocSearch-Commands-Key">
<CommandIcon ariaLabel={navigateDownKeyAriaLabel}>
<path d="M7.5 3.5v8M10.5 8.5l-3 3-3-3" />
<path d="M12 5v14" />
<path d="m19 12-7 7-7-7" />
</CommandIcon>
</kbd>
<kbd className="DocSearch-Commands-Key">
<CommandIcon ariaLabel={navigateUpKeyAriaLabel}>
<path d="M7.5 11.5v-8M10.5 6.5l-3-3-3 3" />
<path d="m5 12 7-7 7 7" />
<path d="M12 19V5" />
</CommandIcon>
</kbd>
<span className="DocSearch-Label">{navigateText}</span>
</li>
<li>
<kbd className="DocSearch-Commands-Key">
<CommandIcon ariaLabel={closeKeyAriaLabel}>
<path d="M13.6167 8.936c-.1065.3583-.6883.962-1.4875.962-.7993 0-1.653-.9165-1.653-2.1258v-.5678c0-1.2548.7896-2.1016 1.653-2.1016.8634 0 1.3601.4778 1.4875 1.0724M9 6c-.1352-.4735-.7506-.9219-1.46-.8972-.7092.0246-1.344.57-1.344 1.2166s.4198.8812 1.3445.9805C8.465 7.3992 8.968 7.9337 9 8.5c.032.5663-.454 1.398-1.4595 1.398C6.6593 9.898 6 9 5.963 8.4851m-1.4748.5368c-.2635.5941-.8099.876-1.5443.876s-1.7073-.6248-1.7073-2.204v-.4603c0-1.0416.721-2.131 1.7073-2.131.9864 0 1.6425 1.031 1.5443 2.2492h-2.956" />
<CommandIcon ariaLabel={selectKeyAriaLabel}>
<polyline points="9 10 4 15 9 20" />
<path d="M20 4v7a4 4 0 0 1-4 4H4" />
</CommandIcon>
</kbd>
<span className="DocSearch-Label">{closeText}</span>
<span className="DocSearch-Label">{selectText}</span>
</li>
<li>
<kbd className="DocSearch-Commands-Key">
<span className="DocSearch-Escape-Key">ESC</span>
</kbd>
<span className="DocSearch-Label" aria-label={closeKeyAriaLabel}>
{closeText}
</span>
</li>
</ul>
</>

View file

@ -1,6 +1,6 @@
import React, { type JSX } from 'react';
import { NoResultsIcon } from './icons';
import { NoResultsIcon, SearchIcon } from './icons';
import type { ScreenStateProps } from './ScreenState';
import type { InternalDocSearchHit } from './types';
@ -17,7 +17,7 @@ type NoResultsScreenProps = Omit<ScreenStateProps<InternalDocSearchHit>, 'transl
export function NoResultsScreen({ translations = {}, ...props }: NoResultsScreenProps): JSX.Element {
const {
noResultsText = 'No results for',
noResultsText = 'No results found for',
suggestedQueryText = 'Try searching for',
reportMissingResultsText = 'Believe this query should return results?',
reportMissingResultsLinkText = 'Let us know.',
@ -41,6 +41,7 @@ export function NoResultsScreen({ translations = {}, ...props }: NoResultsScreen
(acc, search) => [
...acc,
<li key={search}>
<SearchIcon size={16} />
<button
className="DocSearch-Prefill"
key={search}

View file

@ -2,16 +2,14 @@ import type { AutocompleteApi, AutocompleteState } from '@algolia/autocomplete-c
import React, { type JSX, type RefObject } from 'react';
import { MAX_QUERY_SIZE } from './constants';
import { LoadingIcon } from './icons/LoadingIcon';
import { ResetIcon } from './icons/ResetIcon';
import { SearchIcon } from './icons/SearchIcon';
import { LoadingIcon, CloseIcon, SearchIcon } from './icons';
import type { InternalDocSearchHit } from './types';
export type SearchBoxTranslations = Partial<{
resetButtonTitle: string;
resetButtonAriaLabel: string;
cancelButtonText: string;
cancelButtonAriaLabel: string;
clearButtonTitle: string;
clearButtonAriaLabel: string;
closeButtonText: string;
closeButtonAriaLabel: string;
searchInputLabel: string;
}>;
@ -27,10 +25,10 @@ interface SearchBoxProps
export function SearchBox({ translations = {}, ...props }: SearchBoxProps): JSX.Element {
const {
resetButtonTitle = 'Clear the query',
resetButtonAriaLabel = 'Clear the query',
cancelButtonText = 'Cancel',
cancelButtonAriaLabel = 'Cancel',
clearButtonTitle = 'Clear',
clearButtonAriaLabel = 'Clear the query',
closeButtonText = 'Close',
closeButtonAriaLabel = 'Close',
searchInputLabel = 'Search',
} = translations;
const { onReset } = props.getFormProps({
@ -77,20 +75,31 @@ export function SearchBox({ translations = {}, ...props }: SearchBoxProps): JSX.
})}
/>
<button
type="reset"
title={resetButtonTitle}
className="DocSearch-Reset"
aria-label={resetButtonAriaLabel}
hidden={!props.state.query}
>
<ResetIcon />
</button>
</form>
<div className="DocSearch-Actions">
<button
className="DocSearch-Clear"
type="reset"
aria-label={clearButtonAriaLabel}
hidden={!props.state.query}
tabIndex={props.state.query ? 0 : -1}
aria-hidden={!props.state.query ? 'true' : 'false'}
>
{clearButtonTitle}
</button>
<button className="DocSearch-Cancel" type="reset" aria-label={cancelButtonAriaLabel} onClick={props.onClose}>
{cancelButtonText}
</button>
<div className="DocSearch-Divider" />
<button
type="button"
title={closeButtonText}
className="DocSearch-Close"
aria-label={closeButtonAriaLabel}
onClick={props.onClose}
>
<CloseIcon />
</button>
</div>
</form>
</>
);
}

View file

@ -1,6 +1,6 @@
import React, { type JSX } from 'react';
import { RecentIcon, ResetIcon, StarIcon } from './icons';
import { RecentIcon, CloseIcon, StarIcon, SearchIcon } from './icons';
import { Results } from './Results';
import type { ScreenStateProps } from './ScreenState';
import type { InternalDocSearchHit } from './types';
@ -22,7 +22,7 @@ type StartScreenProps = Omit<ScreenStateProps<InternalDocSearchHit>, 'translatio
export function StartScreen({ translations = {}, ...props }: StartScreenProps): JSX.Element | null {
const {
recentSearchesTitle = 'Recent',
noRecentSearchesText = 'No recent searches',
noRecentSearchesText = 'Make a search to see results',
saveRecentSearchButtonTitle = 'Save this search',
removeRecentSearchButtonTitle = 'Remove this search from history',
favoriteSearchesTitle = 'Favorite',
@ -35,6 +35,9 @@ export function StartScreen({ translations = {}, ...props }: StartScreenProps):
return (
<div className="DocSearch-StartScreen">
<div className="DocSearch-StartScreen-Icon">
<SearchIcon size={64} color="#5a5e9a" />
</div>
<p className="DocSearch-Help">{noRecentSearchesText}</p>
</div>
);
@ -89,7 +92,7 @@ export function StartScreen({ translations = {}, ...props }: StartScreenProps):
});
}}
>
<ResetIcon />
<CloseIcon />
</button>
</div>
</>
@ -120,7 +123,7 @@ export function StartScreen({ translations = {}, ...props }: StartScreenProps):
});
}}
>
<ResetIcon />
<CloseIcon />
</button>
</div>
)}

View file

@ -134,10 +134,10 @@ describe('api', () => {
translations={{
modal: {
searchBox: {
resetButtonTitle: 'Effacer',
resetButtonAriaLabel: 'Effacer',
cancelButtonText: 'Annuler',
cancelButtonAriaLabel: 'Annuler',
clearButtonTitle: 'Effacer',
clearButtonAriaLabel: 'Effacer',
closeButtonText: 'Fermer',
closeButtonAriaLabel: 'Fermer',
searchInputLabel: 'Recherche',
},
},
@ -153,10 +153,10 @@ describe('api', () => {
expect(document.querySelector(docSearchSelector)).toBeInTheDocument();
expect(document.querySelector('.DocSearch-Cancel')?.innerHTML).toBe('Annuler');
expect(document.querySelector('.DocSearch-Cancel')?.getAttribute('aria-label')).toBe('Annuler');
expect(document.querySelector('.DocSearch-Reset')?.getAttribute('title')).toBe('Effacer');
expect(document.querySelector('.DocSearch-Reset')?.getAttribute('aria-label')).toBe('Effacer');
expect(document.querySelector('.DocSearch-Clear')?.innerHTML).toBe('Effacer');
expect(document.querySelector('.DocSearch-Clear')?.getAttribute('aria-label')).toBe('Effacer');
expect(document.querySelector('.DocSearch-Close')?.getAttribute('title')).toBe('Fermer');
expect(document.querySelector('.DocSearch-Close')?.getAttribute('aria-label')).toBe('Fermer');
expect(searchInputLabel?.textContent).toBe('Recherche');
});
@ -170,9 +170,9 @@ describe('api', () => {
closeKeyAriaLabel: "Touche d'échappement",
navigateText: 'Pour naviguer',
navigateUpKeyAriaLabel: 'Flèche vers le haut',
navigateDownKeyAriaLabel: 'Flèche le bas',
searchByText: 'Recherche par',
selectText: 'Pour selectionner',
navigateDownKeyAriaLabel: 'Flèche vers le bas',
poweredByText: 'Propulsé par',
selectText: 'Pour sélectionner',
selectKeyAriaLabel: "Touche d'entrée",
},
},
@ -186,17 +186,18 @@ describe('api', () => {
fireEvent.click(await screen.findByText('Search'));
});
expect(screen.getByText('Recherche par')).toBeInTheDocument();
expect(screen.getByText('Pour fermer')).toBeInTheDocument();
expect(screen.getByText('Pour naviguer')).toBeInTheDocument();
expect(screen.getByText('Pour selectionner')).toBeInTheDocument();
expect(
document.querySelector('.DocSearch-Commands-Key > svg[aria-label="Touche d\'échappement"]'),
).toBeInTheDocument();
await screen.findByText('Propulsé par');
await screen.findByText('Pour fermer');
await screen.findByText('Pour naviguer');
await screen.findByText('Pour sélectionner');
expect(screen.getByLabelText("Touche d'échappement")).toBeInTheDocument();
expect(
document.querySelector('.DocSearch-Commands-Key > svg[aria-label="Flèche vers le haut"]'),
).toBeInTheDocument();
expect(document.querySelector('.DocSearch-Commands-Key > svg[aria-label="Flèche le bas"]')).toBeInTheDocument();
expect(
document.querySelector('.DocSearch-Commands-Key > svg[aria-label="Flèche vers le bas"]'),
).toBeInTheDocument();
expect(
document.querySelector('.DocSearch-Commands-Key > svg[aria-label="Touche d\'entrée"]'),
).toBeInTheDocument();
@ -228,7 +229,7 @@ describe('api', () => {
});
});
expect(screen.getByText(/No results for/)).toBeInTheDocument();
expect(screen.getByText(/No results found for/)).toBeInTheDocument();
expect(document.querySelector('.DocSearch-Help a')).not.toBeInTheDocument();
});
@ -255,7 +256,7 @@ describe('api', () => {
});
});
expect(screen.getByText(/No results for/)).toBeInTheDocument();
expect(screen.getByText(/No results found for/)).toBeInTheDocument();
const link = document.querySelector('.DocSearch-Help a');
expect(link).toBeInTheDocument();
expect(link?.getAttribute('href')).toBe('https://github.com/algolia/docsearch/issues/new?title=q');

View file

@ -1,6 +1,6 @@
import React, { type JSX } from 'react';
export function ResetIcon(): JSX.Element {
export function CloseIcon(): JSX.Element {
return (
<svg width="20" height="20" viewBox="0 0 20 20">
<path

View file

@ -0,0 +1,16 @@
import React, { type JSX } from 'react';
export function MetaKeyIcon(): JSX.Element {
return (
<svg width="20" height="20" viewBox="0 0 24 24" className="DocSearch-Meta-Key-Icon">
<path
d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}

View file

@ -3,16 +3,20 @@ import React, { type JSX } from 'react';
export function NoResultsIcon(): JSX.Element {
return (
<svg
width="40"
height="40"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
viewBox="0 0 24 24"
fill="none"
fillRule="evenodd"
stroke="currentColor"
stroke="#5a5e9a"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M15.5 4.8c2 3 1.7 7-1 9.7h0l4.3 4.3-4.3-4.3a7.8 7.8 0 01-9.8 1m-2.2-2.2A7.8 7.8 0 0113.2 2.4M2 18L18 2"></path>
<path d="m13.5 8.5-5 5" />
<path d="m8.5 8.5 5 5" />
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.3-4.3" />
</svg>
);
}

View file

@ -1,16 +1,15 @@
import React, { type JSX } from 'react';
export function SearchIcon(): JSX.Element {
interface SearchIconProps {
size?: number;
color?: string;
}
export function SearchIcon({ size = 20, color = 'currentColor' }: SearchIconProps): JSX.Element {
return (
<svg width="20" height="20" className="DocSearch-Search-Icon" viewBox="0 0 20 20" aria-hidden="true">
<path
d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z"
stroke="currentColor"
fill="none"
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
/>
<svg width={size} height={size} className="DocSearch-Search-Icon" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="11" cy="11" r="8" stroke={color} fill="none" strokeWidth="1.4" />
<path d="m21 21-4.3-4.3" stroke={color} fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}

View file

@ -2,14 +2,22 @@ import React, { type JSX } from 'react';
const LvlIcon: React.FC = () => {
return (
<svg width="20" height="20" viewBox="0 0 20 20">
<path
d="M17 6v12c0 .52-.2 1-1 1H4c-.7 0-1-.33-1-1V2c0-.55.42-1 1-1h8l5 5zM14 8h-3.13c-.51 0-.87-.34-.87-.87V4"
stroke="currentColor"
fill="none"
fillRule="evenodd"
strokeLinejoin="round"
/>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
<path d="M14 2v4a2 2 0 0 0 2 2h4" />
<path d="M10 9H8" />
<path d="M16 13H8" />
<path d="M16 17H8" />
</svg>
);
};
@ -27,15 +35,20 @@ export function SourceIcon(props: { type: string }): JSX.Element {
function AnchorIcon(): JSX.Element {
return (
<svg width="20" height="20" viewBox="0 0 20 20">
<path
d="M13 13h4-4V8H7v5h6v4-4H7V8H3h4V3v5h6V3v5h4-4v5zm-6 0v4-4H3h4z"
stroke="currentColor"
fill="none"
fillRule="evenodd"
strokeLinecap="round"
strokeLinejoin="round"
/>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="4" x2="20" y1="9" y2="9" />
<line x1="4" x2="20" y1="15" y2="15" />
<line x1="10" x2="8" y1="3" y2="21" />
<line x1="16" x2="14" y1="3" y2="21" />
</svg>
);
}

View file

@ -1,7 +1,7 @@
export * from './GoToExternalIcon';
export * from './LoadingIcon';
export * from './RecentIcon';
export * from './ResetIcon';
export * from './CloseIcon';
export * from './SearchIcon';
export * from './SelectIcon';
export * from './SourceIcon';

View file

@ -187,14 +187,14 @@ const translations: DocSearchTranslations = {
helpText: 'You might want to check your network connection.',
},
footer: {
selectText: 'to select',
selectText: 'Select',
selectKeyAriaLabel: 'Enter key',
navigateText: 'to navigate',
navigateText: 'Navigate',
navigateUpKeyAriaLabel: 'Arrow up',
navigateDownKeyAriaLabel: 'Arrow down',
closeText: 'to close',
closeText: 'Close',
closeKeyAriaLabel: 'Escape key',
searchByText: 'Search by',
poweredByText: 'Powered by',
},
noResultsScreen: {
noResultsText: 'No results for',
@ -253,10 +253,7 @@ docsearch({
When provided, an informative message wrapped with your link will be displayed on no results searches. The default text can be changed using the [translations](#translations) property.
<div className="uil-ta-center">
<img
src={useBaseUrl('img/assets/noResultsScreen.png')}
alt="No results screen with informative message"
/>
<img src={useBaseUrl('img/assets/noResultsScreen.png')} alt="No results screen with informative message" />
</div>
## `resultsFooterComponent`