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

View file

@ -24,24 +24,16 @@
.showcaseCardTitle a {
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 {
display: block;
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 a:not(:focus):hover {
background-size: 100% 1px;
}
.showcaseCardTitle,
@ -57,10 +49,7 @@
margin-left: 6px;
padding-left: 12px;
padding-right: 12px;
z-index: 1;
flex-grow: 0;
flex-shrink: 0;
border: 0;
border: none;
}
html[data-theme='dark'] .showcaseCardSrcBtn {
@ -72,11 +61,6 @@ html[data-theme='dark'] .showcaseCardSrcBtn:hover {
background-color: var(--ifm-color-emphasis-300) !important;
}
.showcaseCardSrcBtn:focus,
.showcaseCardTitle a:focus {
outline: none;
}
.showcaseCardSrcBtn:focus-visible {
background-color: var(--ifm-color-secondary-dark);
}
@ -87,37 +71,23 @@ html[data-theme='dark'] .showcaseCardSrcBtn:hover {
}
.cardFooter {
list-style: none;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.tag {
font-size: 0.675rem;
border: 1px solid var(--ifm-color-secondary-darkest);
white-space: nowrap;
cursor: default;
margin-right: 6px;
margin-bottom: 6px !important;
border-radius: 12px;
display: inline-flex;
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 {
overflow: hidden;
margin-left: 8px;
white-space: nowrap;
}
.tag .colorLabel {

View file

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

View file

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

View file

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

View file

@ -9,38 +9,30 @@ input[type='checkbox'] + .checkboxLabel {
display: flex;
align-items: center;
cursor: pointer;
overflow: hidden;
line-height: 1.5;
margin: 0;
border-radius: 4px;
text-overflow: ellipsis;
padding: 0.275rem 0.8rem;
white-space: nowrap;
opacity: 0.85;
transition: opacity 200ms ease-out;
border: 2px solid var(--ifm-color-secondary-darkest);
background-color: inherit;
}
input:focus + .checkboxLabel,
input:focus-visible + .checkboxLabel,
.checkboxLabel:hover {
opacity: 1;
outline: 0;
box-shadow: 0px 0px 2px 1px var(--ifm-color-secondary-darkest);
}
input:focus-visible + .checkboxLabel {
outline: 2px solid currentColor;
}
input:checked + .checkboxLabel {
opacity: 0.9;
transition: opacity 200ms ease-out;
background-color: var(--site-color-checkbox-checked-bg);
border: 2px solid var(--ifm-color-primary-darkest);
}
input:checked:focus + .checkboxLabel,
input:checked:focus-visible + .checkboxLabel,
input:checked + .checkboxLabel:hover {
outline: 0;
opacity: 0.75;
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 tooltipId = `${id}_tooltip`;
useEffect(() => {
if (anchorEl) {
@ -116,12 +117,13 @@ export default function Tooltip({
<>
{React.cloneElement(children, {
ref: setReferenceElement,
'aria-describedby': open ? tooltipId : undefined,
})}
{container
? ReactDOM.createPortal(
open && (
<div
id={id}
id={tooltipId}
role="tooltip"
ref={setPopperElement}
className={styles.tooltip}

View file

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

View file

@ -22,6 +22,7 @@ import ShowcaseCard from './_components/ShowcaseCard';
import {sortedUsers, Tags, TagList, User, TagType} from '@site/src/data/users';
import ShowcaseTooltip from './_components/ShowcaseTooltip';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import {useLocation} from '@docusaurus/router';
import styles from './styles.module.css';
@ -31,6 +32,31 @@ const DESCRIPTION = 'List of websites people are building with Docusaurus';
const EDIT_URL =
'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(
users: User[],
selectedTags: TagType[],
@ -53,10 +79,11 @@ function filterUsers(
function useFilteredUsers() {
const selectedTags = useSelectedTags();
const location = useLocation();
const location = useLocation<UserState>();
const [operator, setOperator] = useState<Operator>('OR');
useEffect(() => {
setOperator(readOperator(location.search));
restoreUserState(location.state);
}, [location]);
return useMemo(
() => filterUsers(sortedUsers, selectedTags, operator),
@ -66,15 +93,15 @@ function useFilteredUsers() {
function useSelectedTags() {
// 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
const [selectedTags, setSelectedTags] = useState<TagType[]>([]);
// Sync tags from QS to state (delayed on purpose to avoid SSR/Client hydration mismatch)
useEffect(() => {
const tags = readSearchTags(location.search);
setSelectedTags(tags);
setSelectedTags(readSearchTags(location.search));
restoreUserState(location.state);
}, [location]);
return selectedTags;
@ -101,18 +128,18 @@ function ShowcaseFilters() {
return (
<section className="container margin-top--l margin-bottom--lg">
<div className={clsx('margin-bottom--sm', styles.filterCheckbox)}>
<span>
<div>
<h2>Filters</h2>
<span>{`(${filteredUsers.length} site${
filteredUsers.length > 1 ? 's' : ''
})`}</span>
</span>
</div>
<ShowcaseFilterToggle />
</div>
<ul className={styles.checkboxList}>
{TagList.map((tag, i) => {
const {label, description, color} = Tags[tag];
const id = `showcase_checkbox_id_${tag};`;
const id = `showcase_checkbox_id_${tag}`;
return (
<li key={i} className={styles.checkboxListItem}>

View file

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