mirror of
https://github.com/facebook/docusaurus.git
synced 2025-08-02 00:09:48 +02:00
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:
parent
8359ff36cd
commit
d25bf24753
10 changed files with 88 additions and 101 deletions
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
font-weight: 500;
|
||||
max-width: 300px;
|
||||
opacity: 0.92;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.tooltipArrow {
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue