mirror of
https://github.com/LukeHagar/unicorn-utterances.git
synced 2025-12-06 12:57:44 +00:00
chore: initial work to migrate to react aria
This commit is contained in:
@@ -33,7 +33,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ["*.ts"],
|
files: ["*.ts", "*.tsx"],
|
||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
extends: ["plugin:@typescript-eslint/recommended"],
|
extends: ["plugin:@typescript-eslint/recommended"],
|
||||||
rules: {
|
rules: {
|
||||||
|
|||||||
@@ -46,7 +46,13 @@ export default defineConfig({
|
|||||||
vite: {
|
vite: {
|
||||||
ssr: {
|
ssr: {
|
||||||
external: ["svgo"],
|
external: ["svgo"],
|
||||||
noExternal: ["@floating-ui/react", "@floating-ui/react-dom"],
|
noExternal: [
|
||||||
|
"react-aria",
|
||||||
|
"react-stately",
|
||||||
|
/@react-aria/,
|
||||||
|
/@react-stately/,
|
||||||
|
/@react-types/,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
plugins: [svgr()],
|
plugins: [svgr()],
|
||||||
},
|
},
|
||||||
|
|||||||
1608
package-lock.json
generated
1608
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -117,7 +117,8 @@
|
|||||||
"*.{js,ts,astro,jsx}": "prettier --write"
|
"*.{js,ts,astro,jsx}": "prettier --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/react": "^0.24.3",
|
"react-aria": "^3.26.0",
|
||||||
|
"react-stately": "^3.24.0",
|
||||||
"medium-zoom": "^1.0.8",
|
"medium-zoom": "^1.0.8",
|
||||||
"preact": "^10.16.0"
|
"preact": "^10.16.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,68 +1,97 @@
|
|||||||
@import "src/tokens/index";
|
@import "src/tokens/index";
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--page-popup_padding: var(--spc-6x);
|
--page-popup_padding: var(--spc-6x);
|
||||||
--page-popup_gap: var(--spc-4x);
|
--page-popup_gap: var(--spc-4x);
|
||||||
--page-popup_offset: var(--spc-8x);
|
--page-popup_offset: var(--spc-8x);
|
||||||
--page-popup_corner-radius: var(--corner-radius_l);
|
--page-popup_corner-radius: var(--corner-radius_l);
|
||||||
--page-popup_border-width: var(--border-width_m);
|
--page-popup_border-width: var(--border-width_m);
|
||||||
|
|
||||||
--page-popup_background-color: var(--background_primary);
|
--page-popup_background-color: var(--background_primary);
|
||||||
--page-popup_border-color: var(--primary_variant);
|
--page-popup_border-color: var(--primary_variant);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup {
|
.popup {
|
||||||
background: var(--page-popup_background-color);
|
background: var(--page-popup_background-color);
|
||||||
border: var(--page-popup_border-width) solid var(--page-popup_border-color);
|
border: var(--page-popup_border-width) solid var(--page-popup_border-color);
|
||||||
box-shadow: var(--shadow_popup_light);
|
box-shadow: var(--shadow_popup_light);
|
||||||
padding: var(--page-popup_padding);
|
padding: var(--page-popup_padding);
|
||||||
min-width: 13.5rem;
|
min-width: 13.5rem;
|
||||||
border-radius: var(--page-popup_corner-radius);
|
border-radius: var(--page-popup_corner-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popupInner {
|
.popupInner {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--page-popup_gap);
|
gap: var(--page-popup_gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popupInput {
|
.popupInput {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
min-width: calc(3ch + calc(var(--form-field_padding-horizontal) * 2));
|
min-width: calc(3ch + calc(var(--form-field_padding-horizontal) * 2));
|
||||||
-moz-appearance: textfield;
|
-moz-appearance: textfield;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popupInput::-webkit-outer-spin-button,
|
.popupInput::-webkit-outer-spin-button,
|
||||||
.popupInput::-webkit-inner-spin-button {
|
.popupInput::-webkit-inner-spin-button {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popupTopArea {
|
.popupTopArea {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--page-popup_gap);
|
gap: var(--page-popup_gap);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonContainer {
|
.buttonContainer {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconButton {
|
.iconButton {
|
||||||
height: calc(var(--form-field_padding-vertical) * 2 + var(--p_medium_line-height));
|
height: calc(
|
||||||
width: calc(var(--form-field_padding-vertical) * 2 + var(--p_medium_line-height));
|
var(--form-field_padding-vertical) * 2 + var(--p_medium_line-height)
|
||||||
|
);
|
||||||
|
width: calc(
|
||||||
|
var(--form-field_padding-vertical) * 2 + var(--p_medium_line-height)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonContainer svg {
|
.buttonContainer svg {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup > svg {
|
.underlay {
|
||||||
fill: var(--page-popup_background-color);
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow[data-placement="top"] {
|
||||||
|
top: 100%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow[data-placement="bottom"] {
|
||||||
|
bottom: 100%;
|
||||||
|
transform: translateX(-50%) rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow[data-placement="left"] {
|
||||||
|
left: 100%;
|
||||||
|
transform: translateY(-50%) rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow[data-placement="right"] {
|
||||||
|
right: 100%;
|
||||||
|
transform: translateY(-50%) rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,6 @@
|
|||||||
import {
|
|
||||||
arrow,
|
|
||||||
FloatingArrow,
|
|
||||||
FloatingFocusManager,
|
|
||||||
offset,
|
|
||||||
useClick,
|
|
||||||
useDismiss,
|
|
||||||
useFloating,
|
|
||||||
useInteractions,
|
|
||||||
useRole,
|
|
||||||
} from "@floating-ui/react";
|
|
||||||
import { useRef, useState } from "preact/hooks";
|
import { useRef, useState } from "preact/hooks";
|
||||||
import { Fragment } from "preact";
|
import { Fragment, RefObject } from "preact";
|
||||||
import { createPortal, StateUpdater } from "preact/compat";
|
import { createPortal } from "preact/compat";
|
||||||
import mainStyles from "./pagination.module.scss";
|
import mainStyles from "./pagination.module.scss";
|
||||||
import more from "src/icons/more_horiz.svg?raw";
|
import more from "src/icons/more_horiz.svg?raw";
|
||||||
import { PaginationProps } from "components/pagination/types";
|
import { PaginationProps } from "components/pagination/types";
|
||||||
@@ -20,10 +9,19 @@ import { Button, IconOnlyButton } from "components/button/button";
|
|||||||
import subtract from "../../icons/subtract.svg?raw";
|
import subtract from "../../icons/subtract.svg?raw";
|
||||||
import add from "../../icons/add.svg?raw";
|
import add from "../../icons/add.svg?raw";
|
||||||
import { Input } from "components/input/input";
|
import { Input } from "components/input/input";
|
||||||
|
import {
|
||||||
|
useDialog,
|
||||||
|
useOverlayTrigger,
|
||||||
|
usePopover,
|
||||||
|
Overlay,
|
||||||
|
DismissButton,
|
||||||
|
} from "react-aria";
|
||||||
|
import { OverlayTriggerState, useOverlayTriggerState } from "react-stately";
|
||||||
|
import { DOMProps } from "@react-types/shared";
|
||||||
|
|
||||||
function PopupContents(
|
function PopupContents(
|
||||||
props: Pick<PaginationProps, "page" | "getPageHref" | "softNavigate"> & {
|
props: Pick<PaginationProps, "page" | "getPageHref" | "softNavigate"> & {
|
||||||
setIsOpen: StateUpdater<boolean>;
|
close: () => void;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const [count, setCount] = useState(props.page.currentPage);
|
const [count, setCount] = useState(props.page.currentPage);
|
||||||
@@ -35,7 +33,7 @@ function PopupContents(
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (props.softNavigate) {
|
if (props.softNavigate) {
|
||||||
props.softNavigate(props.getPageHref(count));
|
props.softNavigate(props.getPageHref(count));
|
||||||
props.setIsOpen(false);
|
props.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
location.href = props.getPageHref(count);
|
location.href = props.getPageHref(count);
|
||||||
@@ -59,7 +57,7 @@ function PopupContents(
|
|||||||
data-testid="pagination-popup-input"
|
data-testid="pagination-popup-input"
|
||||||
class={style.popupInput}
|
class={style.popupInput}
|
||||||
value={count}
|
value={count}
|
||||||
onChange={(e) => {
|
onInput={(e) => {
|
||||||
const newVal = (e.target as HTMLInputElement).valueAsNumber;
|
const newVal = (e.target as HTMLInputElement).valueAsNumber;
|
||||||
if (newVal > props.page.lastPage) {
|
if (newVal > props.page.lastPage) {
|
||||||
setCount(props.page.lastPage);
|
setCount(props.page.lastPage);
|
||||||
@@ -97,75 +95,104 @@ function PopupContents(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PaginationPopoverProps
|
||||||
|
extends Pick<PaginationProps, "page" | "getPageHref" | "softNavigate"> {
|
||||||
|
triggerRef: RefObject<Element>;
|
||||||
|
state: OverlayTriggerState;
|
||||||
|
overlayProps: DOMProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationPopover({
|
||||||
|
triggerRef,
|
||||||
|
state,
|
||||||
|
overlayProps,
|
||||||
|
...props
|
||||||
|
}: PaginationPopoverProps) {
|
||||||
|
/* Setup popover */
|
||||||
|
const popoverRef = useRef(null);
|
||||||
|
const { popoverProps, underlayProps, arrowProps, placement } = usePopover(
|
||||||
|
{
|
||||||
|
shouldFlip: true,
|
||||||
|
offset: 32 - 14 / 2,
|
||||||
|
popoverRef,
|
||||||
|
triggerRef,
|
||||||
|
},
|
||||||
|
state
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Setup dialog */
|
||||||
|
const dialogRef = useRef(null);
|
||||||
|
const { dialogProps, titleProps } = useDialog(overlayProps, dialogRef);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Overlay>
|
||||||
|
<div {...underlayProps} className={style.underlay} />
|
||||||
|
|
||||||
|
<div {...popoverProps} ref={popoverRef} className={style.popup}>
|
||||||
|
<svg
|
||||||
|
width="24"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 14"
|
||||||
|
fill="none"
|
||||||
|
{...arrowProps}
|
||||||
|
className={style.arrow}
|
||||||
|
data-placement={placement}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M9.6 12.8L0 0H24L14.4 12.8C13.2 14.4 10.8 14.4 9.6 12.8Z"
|
||||||
|
fill="var(--page-popup_background-color)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M2.5 2.08616e-06L11.2 11.6C11.6 12.1333 12.4 12.1333 12.8 11.6L21.5 2.08616e-06L24 0L14.4 12.8C13.2 14.4 10.8 14.4 9.6 12.8L0 2.08616e-06H2.5Z"
|
||||||
|
fill="var(--page-popup_border-color)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<DismissButton onDismiss={state.close} />
|
||||||
|
<div {...dialogProps} ref={dialogRef}>
|
||||||
|
<h1 {...titleProps} className="visually-hidden">
|
||||||
|
Go to page
|
||||||
|
</h1>
|
||||||
|
<PopupContents {...props} close={state.close} />
|
||||||
|
</div>
|
||||||
|
<DismissButton onDismiss={state.close} />
|
||||||
|
</div>
|
||||||
|
</Overlay>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function PaginationMenuAndPopover(
|
export function PaginationMenuAndPopover(
|
||||||
props: Pick<PaginationProps, "page" | "getPageHref" | "softNavigate">
|
props: Pick<PaginationProps, "page" | "getPageHref" | "softNavigate">
|
||||||
) {
|
) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
/* Setup trigger */
|
||||||
const arrowRef = useRef(null);
|
const triggerRef = useRef(null);
|
||||||
|
const state = useOverlayTriggerState({});
|
||||||
const { refs, floatingStyles, context } = useFloating({
|
const { triggerProps, overlayProps } = useOverlayTrigger(
|
||||||
open: isOpen,
|
{ type: "dialog" },
|
||||||
placement: "top",
|
state,
|
||||||
onOpenChange: setIsOpen,
|
triggerRef
|
||||||
middleware: [
|
|
||||||
offset(32 - 14 / 2),
|
|
||||||
arrow({
|
|
||||||
element: arrowRef,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const click = useClick(context);
|
|
||||||
const dismiss = useDismiss(context);
|
|
||||||
const role = useRole(context);
|
|
||||||
|
|
||||||
const { getReferenceProps, getFloatingProps } = useInteractions([
|
|
||||||
click,
|
|
||||||
dismiss,
|
|
||||||
role,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const portal = createPortal(
|
|
||||||
<FloatingFocusManager
|
|
||||||
context={context}
|
|
||||||
order={["floating", "content"]}
|
|
||||||
modal={false}
|
|
||||||
returnFocus={false}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={refs.setFloating}
|
|
||||||
style={floatingStyles as never}
|
|
||||||
{...getFloatingProps()}
|
|
||||||
class={style.popup}
|
|
||||||
>
|
|
||||||
<PopupContents {...props} setIsOpen={setIsOpen} />
|
|
||||||
<FloatingArrow
|
|
||||||
ref={arrowRef}
|
|
||||||
context={context}
|
|
||||||
height={14}
|
|
||||||
width={24}
|
|
||||||
stroke={"var(--page-popup_border-color)"}
|
|
||||||
strokeWidth={2}
|
|
||||||
tipRadius={1.5}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</FloatingFocusManager>,
|
|
||||||
document.querySelector("body")
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<li className={`${mainStyles.paginationItem}`}>
|
<li className={`${mainStyles.paginationItem}`}>
|
||||||
|
{/* Add onClick since onPress doesn't work with Preact well */}
|
||||||
<button
|
<button
|
||||||
|
ref={triggerRef}
|
||||||
|
onClick={triggerProps.onPress as never}
|
||||||
|
{...triggerProps}
|
||||||
data-testid="pagination-menu"
|
data-testid="pagination-menu"
|
||||||
ref={refs.setReference}
|
|
||||||
{...getReferenceProps()}
|
|
||||||
aria-selected={isOpen}
|
|
||||||
className={`text-style-body-medium-bold ${mainStyles.extendPageButton} ${mainStyles.paginationButton} ${mainStyles.paginationIconButton}`}
|
className={`text-style-body-medium-bold ${mainStyles.extendPageButton} ${mainStyles.paginationButton} ${mainStyles.paginationIconButton}`}
|
||||||
dangerouslySetInnerHTML={{ __html: more }}
|
dangerouslySetInnerHTML={{ __html: more }}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
{isOpen && portal}
|
{state.isOpen && (
|
||||||
|
<PaginationPopover
|
||||||
|
{...props}
|
||||||
|
triggerRef={triggerRef}
|
||||||
|
state={state}
|
||||||
|
overlayProps={overlayProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.paginationItem {
|
||||||
|
}
|
||||||
|
|
||||||
.paginationItemExtra {
|
.paginationItemExtra {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
@@ -61,7 +64,6 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.paginationButton.selected {
|
.paginationButton.selected {
|
||||||
background-color: var(--nav-btn_background-color_selected);
|
background-color: var(--nav-btn_background-color_selected);
|
||||||
color: var(--nav-btn_foreground-color_selected);
|
color: var(--nav-btn_foreground-color_selected);
|
||||||
@@ -86,7 +88,8 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paginationButton:disabled, .paginationButton[aria-disabled="true"] {
|
.paginationButton:disabled,
|
||||||
|
.paginationButton[aria-disabled="true"] {
|
||||||
background-color: var(--nav-btn_background-color_disabled);
|
background-color: var(--nav-btn_background-color_disabled);
|
||||||
color: var(--nav-btn_foreground-color_disabled);
|
color: var(--nav-btn_foreground-color_disabled);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user