refactor(website): various fixes and improvements on Showcase page (#5997)

* refactor(website): add various fixes and improvements on Showcase page

* Maintain previous focused element (WIP)

* Fix SSR

* Fix again

* Final fix

Co-authored-by: Josh-Cena <sidachen2003@gmail.com>
This commit is contained in:
Alexey Pyltsyn 2021-11-26 12:45:59 +03:00 committed by GitHub
parent 8359ff36cd
commit d25bf24753
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 88 additions and 101 deletions

View file

@ -16,17 +16,9 @@ import Tooltip from '../ShowcaseTooltip';
import {Tags, TagList, TagType, User, Tag} from '@site/src/data/users'; import {Tags, TagList, TagType, User, Tag} from '@site/src/data/users';
import {sortBy} from '@site/src/utils/jsUtils'; import {sortBy} from '@site/src/utils/jsUtils';
interface Props extends Tag { const TagComp = React.forwardRef<HTMLLIElement, Tag>(
id: string; ({label, color, description}, ref) => (
} <li ref={ref} className={styles.tag} title={description}>
const TagComp = React.forwardRef<HTMLLIElement, Props>(
({id, label, color, description}, ref) => (
<li
ref={ref}
aria-describedby={id}
className={styles.tag}
title={description}>
<span className={styles.textLabel}>{label.toLowerCase()}</span> <span className={styles.textLabel}>{label.toLowerCase()}</span>
<span className={styles.colorLabel} style={{backgroundColor: color}} /> <span className={styles.colorLabel} style={{backgroundColor: color}} />
</li> </li>
@ -52,7 +44,7 @@ function ShowcaseCardTag({tags}: {tags: TagType[]}) {
text={tagObject.description} text={tagObject.description}
anchorEl="#__docusaurus" anchorEl="#__docusaurus"
id={id}> id={id}>
<TagComp id={id} key={index} {...tagObject} /> <TagComp key={index} {...tagObject} />
</Tooltip> </Tooltip>
); );
})} })}
@ -68,10 +60,7 @@ const ShowcaseCard = memo(({user}: {user: User}) => (
<div className="card__body"> <div className="card__body">
<div className={clsx(styles.showcaseCardHeader)}> <div className={clsx(styles.showcaseCardHeader)}>
<h4 className={styles.showcaseCardTitle}> <h4 className={styles.showcaseCardTitle}>
<Link <Link href={user.website} className={styles.showcaseCardLink}>
href={user.website}
tabIndex={0}
className={styles.showcaseCardLink}>
{user.title} {user.title}
</Link> </Link>
</h4> </h4>
@ -81,7 +70,6 @@ const ShowcaseCard = memo(({user}: {user: User}) => (
{user.source && ( {user.source && (
<Link <Link
href={user.source} href={user.source}
tabIndex={0}
className={clsx( className={clsx(
'button button--secondary button--sm', 'button button--secondary button--sm',
styles.showcaseCardSrcBtn, styles.showcaseCardSrcBtn,

View file

@ -24,24 +24,16 @@
.showcaseCardTitle a { .showcaseCardTitle a {
text-decoration: none; text-decoration: none;
position: relative; background: linear-gradient(
var(--ifm-color-primary),
var(--ifm-color-primary)
)
0% 100% / 0% 1px no-repeat;
transition: background-size ease-out 200ms;
} }
.showcaseCardTitle a::after { .showcaseCardTitle a:not(:focus):hover {
display: block; background-size: 100% 1px;
content: '';
position: absolute;
width: 0;
height: 1px;
transition: width ease-out 200ms;
background-color: var(--ifm-color-primary);
bottom: 0;
left: 0;
}
.showcaseCardTitle a:hover::after,
.showcaseCardTitle a:focus-visible::after {
width: 100%;
} }
.showcaseCardTitle, .showcaseCardTitle,
@ -57,10 +49,7 @@
margin-left: 6px; margin-left: 6px;
padding-left: 12px; padding-left: 12px;
padding-right: 12px; padding-right: 12px;
z-index: 1; border: none;
flex-grow: 0;
flex-shrink: 0;
border: 0;
} }
html[data-theme='dark'] .showcaseCardSrcBtn { html[data-theme='dark'] .showcaseCardSrcBtn {
@ -72,11 +61,6 @@ html[data-theme='dark'] .showcaseCardSrcBtn:hover {
background-color: var(--ifm-color-emphasis-300) !important; background-color: var(--ifm-color-emphasis-300) !important;
} }
.showcaseCardSrcBtn:focus,
.showcaseCardTitle a:focus {
outline: none;
}
.showcaseCardSrcBtn:focus-visible { .showcaseCardSrcBtn:focus-visible {
background-color: var(--ifm-color-secondary-dark); background-color: var(--ifm-color-secondary-dark);
} }
@ -87,37 +71,23 @@ html[data-theme='dark'] .showcaseCardSrcBtn:hover {
} }
.cardFooter { .cardFooter {
list-style: none;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center;
} }
.tag { .tag {
font-size: 0.675rem; font-size: 0.675rem;
border: 1px solid var(--ifm-color-secondary-darkest); border: 1px solid var(--ifm-color-secondary-darkest);
white-space: nowrap; cursor: default;
margin-right: 6px; margin-right: 6px;
margin-bottom: 6px !important; margin-bottom: 6px !important;
border-radius: 12px; border-radius: 12px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center;
height: 20px;
cursor: default;
outline: 0px;
text-decoration: none;
z-index: 5;
padding: 0px;
vertical-align: middle;
box-sizing: border-box;
background-color: transparent;
} }
.tag .textLabel { .tag .textLabel {
overflow: hidden;
margin-left: 8px; margin-left: 8px;
white-space: nowrap;
} }
.tag .colorLabel { .tag .colorLabel {

View file

@ -8,6 +8,8 @@
import React, {useState, useEffect, useCallback} from 'react'; import React, {useState, useEffect, useCallback} from 'react';
import {useHistory, useLocation} from '@docusaurus/router'; import {useHistory, useLocation} from '@docusaurus/router';
import {prepareUserState} from '../../index';
import styles from './styles.module.css'; import styles from './styles.module.css';
import clsx from 'clsx'; import clsx from 'clsx';
@ -33,7 +35,11 @@ export default function ShowcaseFilterToggle(): JSX.Element {
const searchParams = new URLSearchParams(location.search); const searchParams = new URLSearchParams(location.search);
searchParams.delete(OperatorQueryKey); searchParams.delete(OperatorQueryKey);
searchParams.append(OperatorQueryKey, operator ? 'OR' : 'AND'); searchParams.append(OperatorQueryKey, operator ? 'OR' : 'AND');
history.push({...location, search: searchParams.toString()}); history.push({
...location,
search: searchParams.toString(),
state: prepareUserState(),
});
}, [operator, location, history]); }, [operator, location, history]);
return ( return (

View file

@ -17,21 +17,22 @@
border: var(--border) solid var(--ifm-color-primary-darkest); border: var(--border) solid var(--ifm-color-primary-darkest);
cursor: pointer; cursor: pointer;
justify-content: space-around; justify-content: space-around;
align-items: center;
opacity: 0.75; opacity: 0.75;
transition: opacity var(--ifm-transition-fast) transition: opacity var(--ifm-transition-fast)
var(--ifm-transition-timing-default); var(--ifm-transition-timing-default);
box-shadow: var(--ifm-global-shadow-md); box-shadow: var(--ifm-global-shadow-md);
} }
input:focus ~ .checkboxLabel,
input:focus-visible ~ .checkboxLabel,
.checkboxLabel:hover { .checkboxLabel:hover {
opacity: 1; opacity: 1;
box-shadow: var(--ifm-global-shadow-md), box-shadow: var(--ifm-global-shadow-md),
0px 0px 2px 1px var(--ifm-color-primary-dark); 0px 0px 2px 1px var(--ifm-color-primary-dark);
} }
input:focus-visible ~ .checkboxLabel::after {
outline: 2px solid currentColor;
}
.checkboxLabel > * { .checkboxLabel > * {
font-size: 0.8rem; font-size: 0.8rem;
color: inherit; color: inherit;

View file

@ -15,6 +15,7 @@ import React, {
} from 'react'; } from 'react';
import {useHistory, useLocation} from '@docusaurus/router'; import {useHistory, useLocation} from '@docusaurus/router';
import {toggleListItem} from '@site/src/utils/jsUtils'; import {toggleListItem} from '@site/src/utils/jsUtils';
import {prepareUserState} from '../../index';
import type {TagType} from '@site/src/data/users'; import type {TagType} from '@site/src/data/users';
import styles from './styles.module.css'; import styles from './styles.module.css';
@ -51,7 +52,11 @@ const ShowcaseTagSelect = React.forwardRef<HTMLLabelElement, Props>(
const tags = readSearchTags(location.search); const tags = readSearchTags(location.search);
const newTags = toggleListItem(tags, tag); const newTags = toggleListItem(tags, tag);
const newSearch = replaceSearchTags(location.search, newTags); const newSearch = replaceSearchTags(location.search, newTags);
history.push({...location, search: newSearch}); history.push({
...location,
search: newSearch,
state: prepareUserState(),
});
}, [tag, location, history]); }, [tag, location, history]);
return ( return (
<> <>
@ -64,15 +69,23 @@ const ShowcaseTagSelect = React.forwardRef<HTMLLabelElement, Props>(
toggleTag(); toggleTag();
} }
}} }}
onFocus={(e) => {
if (e.relatedTarget) {
e.target.nextElementSibling.dispatchEvent(
new KeyboardEvent('focus'),
);
}
}}
onBlur={(e) => {
e.target.nextElementSibling.dispatchEvent(
new KeyboardEvent('blur'),
);
}}
onChange={toggleTag} onChange={toggleTag}
checked={selected} checked={selected}
{...rest} {...rest}
/> />
<label <label ref={ref} htmlFor={id} className={styles.checkboxLabel}>
ref={ref}
htmlFor={id}
className={styles.checkboxLabel}
aria-describedby={id}>
{label} {label}
{icon} {icon}
</label> </label>

View file

@ -9,38 +9,30 @@ input[type='checkbox'] + .checkboxLabel {
display: flex; display: flex;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
overflow: hidden;
line-height: 1.5; line-height: 1.5;
margin: 0;
border-radius: 4px; border-radius: 4px;
text-overflow: ellipsis;
padding: 0.275rem 0.8rem; padding: 0.275rem 0.8rem;
white-space: nowrap;
opacity: 0.85; opacity: 0.85;
transition: opacity 200ms ease-out; transition: opacity 200ms ease-out;
border: 2px solid var(--ifm-color-secondary-darkest); border: 2px solid var(--ifm-color-secondary-darkest);
background-color: inherit;
} }
input:focus + .checkboxLabel,
input:focus-visible + .checkboxLabel,
.checkboxLabel:hover { .checkboxLabel:hover {
opacity: 1; opacity: 1;
outline: 0;
box-shadow: 0px 0px 2px 1px var(--ifm-color-secondary-darkest); box-shadow: 0px 0px 2px 1px var(--ifm-color-secondary-darkest);
} }
input:focus-visible + .checkboxLabel {
outline: 2px solid currentColor;
}
input:checked + .checkboxLabel { input:checked + .checkboxLabel {
opacity: 0.9; opacity: 0.9;
transition: opacity 200ms ease-out;
background-color: var(--site-color-checkbox-checked-bg); background-color: var(--site-color-checkbox-checked-bg);
border: 2px solid var(--ifm-color-primary-darkest); border: 2px solid var(--ifm-color-primary-darkest);
} }
input:checked:focus + .checkboxLabel,
input:checked:focus-visible + .checkboxLabel,
input:checked + .checkboxLabel:hover { input:checked + .checkboxLabel:hover {
outline: 0;
opacity: 0.75; opacity: 0.75;
box-shadow: 0px 0px 2px 1px var(--ifm-color-primary-dark); box-shadow: 0px 0px 2px 1px var(--ifm-color-primary-dark);
} }

View file

@ -52,6 +52,7 @@ export default function Tooltip({
); );
const timeout = useRef<number>(null); const timeout = useRef<number>(null);
const tooltipId = `${id}_tooltip`;
useEffect(() => { useEffect(() => {
if (anchorEl) { if (anchorEl) {
@ -116,12 +117,13 @@ export default function Tooltip({
<> <>
{React.cloneElement(children, { {React.cloneElement(children, {
ref: setReferenceElement, ref: setReferenceElement,
'aria-describedby': open ? tooltipId : undefined,
})} })}
{container {container
? ReactDOM.createPortal( ? ReactDOM.createPortal(
open && ( open && (
<div <div
id={id} id={tooltipId}
role="tooltip" role="tooltip"
ref={setPopperElement} ref={setPopperElement}
className={styles.tooltip} className={styles.tooltip}

View file

@ -16,7 +16,6 @@
font-weight: 500; font-weight: 500;
max-width: 300px; max-width: 300px;
opacity: 0.92; opacity: 0.92;
white-space: normal;
} }
.tooltipArrow { .tooltipArrow {

View file

@ -22,6 +22,7 @@ import ShowcaseCard from './_components/ShowcaseCard';
import {sortedUsers, Tags, TagList, User, TagType} from '@site/src/data/users'; import {sortedUsers, Tags, TagList, User, TagType} from '@site/src/data/users';
import ShowcaseTooltip from './_components/ShowcaseTooltip'; import ShowcaseTooltip from './_components/ShowcaseTooltip';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import {useLocation} from '@docusaurus/router'; import {useLocation} from '@docusaurus/router';
import styles from './styles.module.css'; import styles from './styles.module.css';
@ -31,6 +32,31 @@ const DESCRIPTION = 'List of websites people are building with Docusaurus';
const EDIT_URL = const EDIT_URL =
'https://github.com/facebook/docusaurus/edit/main/website/src/data/users.tsx'; 'https://github.com/facebook/docusaurus/edit/main/website/src/data/users.tsx';
type UserState = {
scrollTopPosition: number;
focusedElementId: string | undefined;
};
function restoreUserState(userState: UserState | null) {
const {scrollTopPosition, focusedElementId} = userState ?? {
scrollTopPosition: 0,
focusedElementId: undefined,
};
document.getElementById(focusedElementId)?.focus();
window.scrollTo({top: scrollTopPosition});
}
export function prepareUserState(): UserState | undefined {
if (ExecutionEnvironment.canUseDOM) {
return {
scrollTopPosition: window.scrollY,
focusedElementId: document.activeElement?.id,
};
}
return undefined;
}
function filterUsers( function filterUsers(
users: User[], users: User[],
selectedTags: TagType[], selectedTags: TagType[],
@ -53,10 +79,11 @@ function filterUsers(
function useFilteredUsers() { function useFilteredUsers() {
const selectedTags = useSelectedTags(); const selectedTags = useSelectedTags();
const location = useLocation(); const location = useLocation<UserState>();
const [operator, setOperator] = useState<Operator>('OR'); const [operator, setOperator] = useState<Operator>('OR');
useEffect(() => { useEffect(() => {
setOperator(readOperator(location.search)); setOperator(readOperator(location.search));
restoreUserState(location.state);
}, [location]); }, [location]);
return useMemo( return useMemo(
() => filterUsers(sortedUsers, selectedTags, operator), () => filterUsers(sortedUsers, selectedTags, operator),
@ -66,15 +93,15 @@ function useFilteredUsers() {
function useSelectedTags() { function useSelectedTags() {
// The search query-string is the source of truth! // The search query-string is the source of truth!
const location = useLocation(); const location = useLocation<UserState>();
// On SSR / first mount (hydration) no tag is selected // On SSR / first mount (hydration) no tag is selected
const [selectedTags, setSelectedTags] = useState<TagType[]>([]); const [selectedTags, setSelectedTags] = useState<TagType[]>([]);
// Sync tags from QS to state (delayed on purpose to avoid SSR/Client hydration mismatch) // Sync tags from QS to state (delayed on purpose to avoid SSR/Client hydration mismatch)
useEffect(() => { useEffect(() => {
const tags = readSearchTags(location.search); setSelectedTags(readSearchTags(location.search));
setSelectedTags(tags); restoreUserState(location.state);
}, [location]); }, [location]);
return selectedTags; return selectedTags;
@ -101,18 +128,18 @@ function ShowcaseFilters() {
return ( return (
<section className="container margin-top--l margin-bottom--lg"> <section className="container margin-top--l margin-bottom--lg">
<div className={clsx('margin-bottom--sm', styles.filterCheckbox)}> <div className={clsx('margin-bottom--sm', styles.filterCheckbox)}>
<span> <div>
<h2>Filters</h2> <h2>Filters</h2>
<span>{`(${filteredUsers.length} site${ <span>{`(${filteredUsers.length} site${
filteredUsers.length > 1 ? 's' : '' filteredUsers.length > 1 ? 's' : ''
})`}</span> })`}</span>
</span> </div>
<ShowcaseFilterToggle /> <ShowcaseFilterToggle />
</div> </div>
<ul className={styles.checkboxList}> <ul className={styles.checkboxList}>
{TagList.map((tag, i) => { {TagList.map((tag, i) => {
const {label, description, color} = Tags[tag]; const {label, description, color} = Tags[tag];
const id = `showcase_checkbox_id_${tag};`; const id = `showcase_checkbox_id_${tag}`;
return ( return (
<li key={i} className={styles.checkboxListItem}> <li key={i} className={styles.checkboxListItem}>

View file

@ -15,13 +15,13 @@
align-items: center; align-items: center;
} }
.filterCheckbox > span { .filterCheckbox > div:first-child {
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
align-items: center; align-items: center;
} }
.filterCheckbox > span > * { .filterCheckbox > div > * {
margin-bottom: 0; margin-bottom: 0;
margin-right: 8px; margin-right: 8px;
} }
@ -37,20 +37,10 @@
} }
.checkboxListItem { .checkboxListItem {
position: relative;
background-color: transparent;
user-select: none; user-select: none;
white-space: nowrap; white-space: nowrap;
display: inline-flex;
align-items: center;
justify-content: center;
height: 32px; height: 32px;
outline: 0px;
text-decoration: none;
padding: 0px;
font-size: 0.8rem; font-size: 0.8rem;
vertical-align: middle;
box-sizing: border-box;
} }
.checkboxListItem { .checkboxListItem {
@ -66,7 +56,6 @@
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px; gap: 24px;
position: relative;
} }
.showcaseFavorite { .showcaseFavorite {