Cleanup, removal of focus hook. Known bug when using Markdown.
This commit is contained in:
parent
08b182e1c5
commit
4e5c157b15
@ -1,7 +1,6 @@
|
|||||||
import React, { ReactNode } from 'react'
|
import React, { ReactNode } from 'react'
|
||||||
import { usePersonContext } from '../hooks/PersonContext'
|
import { usePersonContext } from '../hooks/PersonContext'
|
||||||
import Container from 'react-bootstrap/esm/Container'
|
import Container from 'react-bootstrap/esm/Container'
|
||||||
import { useAutoFocus } from '../hooks/FocusedElement'
|
|
||||||
import { Col, Row } from 'react-bootstrap'
|
import { Col, Row } from 'react-bootstrap'
|
||||||
|
|
||||||
export function Contact(props: {icon?: string, text: string}) {
|
export function Contact(props: {icon?: string, text: string}) {
|
||||||
@ -30,9 +29,8 @@ export function Contact(props: {icon?: string, text: string}) {
|
|||||||
|
|
||||||
export function Contacts() {
|
export function Contacts() {
|
||||||
const person = usePersonContext()
|
const person = usePersonContext()
|
||||||
const focus = useAutoFocus<HTMLDivElement>('contacts')
|
|
||||||
return (
|
return (
|
||||||
<Container ref={focus} className='contacts' fluid>
|
<Container className='contacts' fluid>
|
||||||
<Row>
|
<Row>
|
||||||
<h2>Contacts</h2>
|
<h2>Contacts</h2>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Container from 'react-bootstrap/Container';
|
import Container from 'react-bootstrap/Container';
|
||||||
import { useAutoFocus } from '../hooks/FocusedElement';
|
|
||||||
import Tag from './Tag';
|
import Tag from './Tag';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
@ -11,10 +10,9 @@ export type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TagCloud(props: Props) {
|
export default function TagCloud(props: Props) {
|
||||||
const focusRef = useAutoFocus<HTMLDivElement>('tags ' + props.title)
|
|
||||||
const containerClasses = ['tag-cloud', 'cloud-' + (props.style || 'standard')]
|
const containerClasses = ['tag-cloud', 'cloud-' + (props.style || 'standard')]
|
||||||
return (
|
return (
|
||||||
<Container ref={focusRef} className={containerClasses.join(' ')}>
|
<Container className={containerClasses.join(' ')}>
|
||||||
<h4>{props.icon && (<i className={'bi-' + props.icon}> </i>)}{props.title}</h4>
|
<h4>{props.icon && (<i className={'bi-' + props.icon}> </i>)}{props.title}</h4>
|
||||||
<Container className='tag-badges'>
|
<Container className='tag-badges'>
|
||||||
{props.tags.map((tag: string) => (<Tag key={tag} text={tag} />) )}
|
{props.tags.map((tag: string) => (<Tag key={tag} text={tag} />) )}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Card from 'react-bootstrap/Card';
|
import Card from 'react-bootstrap/Card';
|
||||||
import { useAutoFocus } from '../../hooks/FocusedElement';
|
|
||||||
import { Placeholder } from 'react-bootstrap';
|
import { Placeholder } from 'react-bootstrap';
|
||||||
import JobTags, { focusKeyPlaceholder } from './JobTags';
|
import JobTags from './JobTags';
|
||||||
import { Job } from '@/PersonalDataTypes';
|
import { Job } from '@/PersonalDataTypes';
|
||||||
import md from '../Markdown';
|
import md from '../Markdown';
|
||||||
|
|
||||||
@ -25,9 +24,8 @@ export function JobCardPlaceholder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function JobCard(props: Props) {
|
export default function JobCard(props: Props) {
|
||||||
const focusRef = useAutoFocus<HTMLDivElement>([props.position, props.company, props.timerange].join(' - '))
|
|
||||||
return (
|
return (
|
||||||
<Card ref={focusRef} className='job-card'>
|
<Card className='job-card'>
|
||||||
{props.heading && (
|
{props.heading && (
|
||||||
<Card.Header>{props.heading}</Card.Header>
|
<Card.Header>{props.heading}</Card.Header>
|
||||||
)}
|
)}
|
||||||
|
@ -1,11 +1,34 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Job } from '@/PersonalDataTypes';
|
import { Job } from '@/PersonalDataTypes';
|
||||||
import { Accordion, Row } from 'react-bootstrap';
|
import { Accordion } from 'react-bootstrap';
|
||||||
import { JobListProps } from './types';
|
import { JobListProps } from './types';
|
||||||
import JobTags from './JobTags';
|
import JobTags from './JobTags';
|
||||||
import md from '../Markdown';
|
import md from '../Markdown';
|
||||||
|
|
||||||
|
function JobTitle(props: {job: Job, heading?: string}) {
|
||||||
|
const {job, heading} = props
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div><strong>{job.position}</strong></div>
|
||||||
|
<div>{[job.company, job.timerange].filter(x => x).join(', ')}</div>
|
||||||
|
{heading && (<div>({heading})</div>)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccordionJobItem(props: {job: Job, eventKey: string, heading?: string}) {
|
||||||
|
const {job, eventKey} = props
|
||||||
|
return (
|
||||||
|
<Accordion.Item eventKey={eventKey}>
|
||||||
|
<Accordion.Header><JobTitle job={job} /></Accordion.Header>
|
||||||
|
<Accordion.Body className='multiline'>
|
||||||
|
{md(job.description)}
|
||||||
|
{job.tags && <JobTags tags={job.tags} />}
|
||||||
|
</Accordion.Body>
|
||||||
|
</Accordion.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
heading: string,
|
heading: string,
|
||||||
@ -19,36 +42,10 @@ const defaultProps = {
|
|||||||
export default function JobsAccordion(props: JobListProps) {
|
export default function JobsAccordion(props: JobListProps) {
|
||||||
const {jobs} = props
|
const {jobs} = props
|
||||||
const config = {...defaultProps, ...props}
|
const config = {...defaultProps, ...props}
|
||||||
function JobTitle(props: {job: Job, heading?: string}) {
|
|
||||||
const {job, heading} = props
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div><strong>{job.position}</strong></div>
|
|
||||||
<div>{[job.company, job.timerange].filter(x => x).join(', ')}</div>
|
|
||||||
{heading && (<div>({heading})</div>)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Accordion defaultActiveKey={jobs.current ? 'current' : 'previous-0'}>
|
<Accordion defaultActiveKey={jobs.current ? 'current' : 'previous-0'}>
|
||||||
{jobs.current && (
|
{jobs.current && <AccordionJobItem job={jobs.current} heading={config.currentHeading} eventKey={'current'}/>}
|
||||||
<Accordion.Item eventKey="current">
|
{jobs.previous?.map((job, index) => <AccordionJobItem key={index} job={job} eventKey={`previous-${index}`} />)}
|
||||||
<Accordion.Header><JobTitle heading={config.currentHeading} job={jobs.current} /> </Accordion.Header>
|
|
||||||
<Accordion.Body>
|
|
||||||
{md(jobs.current.description)}
|
|
||||||
{jobs.current.tags && <JobTags tags={jobs.current.tags} />}
|
|
||||||
</Accordion.Body>
|
|
||||||
</Accordion.Item>
|
|
||||||
)}
|
|
||||||
{jobs.previous?.map((job, index) => (
|
|
||||||
<Accordion.Item eventKey={`previous-${index}`} key={index}>
|
|
||||||
<Accordion.Header><JobTitle job={job} /></Accordion.Header>
|
|
||||||
<Accordion.Body className='multiline'>
|
|
||||||
{md(job.description)}
|
|
||||||
{job.tags && <JobTags tags={job.tags} />}
|
|
||||||
</Accordion.Body>
|
|
||||||
</Accordion.Item>
|
|
||||||
))}
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
import { useState, useRef, useEffect, useCallback } from "react";
|
|
||||||
|
|
||||||
const focusUrlParamName = 'focus'
|
|
||||||
const focusChangedEventName = "focus-changed"
|
|
||||||
const focusedElementClassName = 'focused-element'
|
|
||||||
|
|
||||||
function triggerElementFocused(elementKey?: string) {
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
if (elementKey) {
|
|
||||||
url.searchParams.set(focusUrlParamName, elementKey)
|
|
||||||
} else {
|
|
||||||
url.searchParams.delete(focusUrlParamName)
|
|
||||||
}
|
|
||||||
const focusChangeEvent = new Event(focusChangedEventName)
|
|
||||||
history.pushState({}, "", url);
|
|
||||||
window.dispatchEvent(focusChangeEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useFocusedElement<ElementType extends HTMLElement>(elementKey: string) {
|
|
||||||
const [isFocusedElement, setFocusedElement] = useState(false)
|
|
||||||
const focusedClass = isFocusedElement ? focusedElementClassName : ''
|
|
||||||
const elementRef = useRef<ElementType>(null)
|
|
||||||
|
|
||||||
const focusElement = useCallback(() => {
|
|
||||||
triggerElementFocused(isFocusedElement ? undefined : elementKey)
|
|
||||||
}, [isFocusedElement, elementKey])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
function updateFocusedState() {
|
|
||||||
const params = new URLSearchParams(window.location.search)
|
|
||||||
const focusedElement = params.get(focusUrlParamName)
|
|
||||||
const focused: boolean = focusedElement && focusedElement == elementKey || false
|
|
||||||
setFocusedElement(focused)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFocusedState()
|
|
||||||
addEventListener(focusChangedEventName, updateFocusedState)
|
|
||||||
return () => {
|
|
||||||
removeEventListener(focusChangedEventName, updateFocusedState)
|
|
||||||
}
|
|
||||||
}, [elementKey, setFocusedElement, focusElement])
|
|
||||||
|
|
||||||
isFocusedElement && elementRef.current?.scrollIntoView()
|
|
||||||
|
|
||||||
return {isFocusedElement, focusedClass, elementRef, focusElement}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAutoFocus<ElementType extends HTMLElement>(elementKey: string) {
|
|
||||||
const {elementRef, focusedClass, focusElement} = useFocusedElement<ElementType>(elementKey);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let cleanup = () => {}
|
|
||||||
if (elementRef.current) {
|
|
||||||
const elem = elementRef.current
|
|
||||||
elem.onclick = (evt) => {
|
|
||||||
evt.stopPropagation()
|
|
||||||
focusElement()
|
|
||||||
}
|
|
||||||
const classNameBackup = elem.className
|
|
||||||
elem.className += ' focusable ' + focusedClass
|
|
||||||
cleanup = () => {
|
|
||||||
elem.className = classNameBackup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cleanup
|
|
||||||
}, [elementRef, focusedClass, focusElement])
|
|
||||||
|
|
||||||
return elementRef
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
export function partition<T>(array: T[]|undefined, entriesPerRow: number): T[][] {
|
|
||||||
return array ? array.reduce((accumulator: T[][], current: T, index) => {
|
|
||||||
if (index % entriesPerRow == 0) {
|
|
||||||
accumulator[accumulator.length] = [current]
|
|
||||||
} else {
|
|
||||||
accumulator[accumulator.length - 1]
|
|
||||||
}
|
|
||||||
return accumulator
|
|
||||||
}, []) : []
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>Bootstrap w/ Webpack</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container py-4 px-3 mx-auto">
|
|
||||||
<header class="d-flex justify-content-between align-items-md-center pb-3 mb-5 border-bottom">
|
|
||||||
<h1 class="h4">
|
|
||||||
<a href="/" class="d-flex align-items-center text-dark text-decoration-none">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" class="bi bi-bootstrap-fill d-inline-block me-2" viewBox="0 0 16 16">
|
|
||||||
<path d="M6.375 7.125V4.658h1.78c.973 0 1.542.457 1.542 1.237 0 .802-.604 1.23-1.764 1.23H6.375zm0 3.762h1.898c1.184 0 1.81-.48 1.81-1.377 0-.885-.65-1.348-1.886-1.348H6.375v2.725z"/>
|
|
||||||
<path d="M4.002 0a4 4 0 0 0-4 4v8a4 4 0 0 0 4 4h8a4 4 0 0 0 4-4V4a4 4 0 0 0-4-4h-8zm1.06 12V3.545h3.399c1.587 0 2.543.809 2.543 2.11 0 .884-.65 1.675-1.483 1.816v.1c1.143.117 1.904.931 1.904 2.033 0 1.488-1.084 2.396-2.888 2.396H5.062z"/>
|
|
||||||
</svg>
|
|
||||||
<span>Webpack</span>
|
|
||||||
</a>
|
|
||||||
</h1>
|
|
||||||
<a href="https://github.com/twbs/examples/tree/main/webpack/" target="_blank" rel="noopener">View on GitHub</a>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<h1>Build Bootstrap with Webpack</h1>
|
|
||||||
<div class="col-lg-8 px-0">
|
|
||||||
<p class="fs-4">You've successfully loaded the Bootstrap + Webpack example! It's loaded up with <a href="https://getbootstrap.com/">Bootstrap 5</a> and uses Webpack to compile and bundle our Sass and JavaScript. It also includes Autoprefixer.</p>
|
|
||||||
<p>If this button appears blue and the link appears purple, you've done it!</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="button" class="btn btn-primary me-3" data-bs-toggle="offcanvas" data-bs-target="#offcanvasExample">Toggle offcanvas</button>
|
|
||||||
<a id="popoverButton" class="text-success" href="#" role="button" data-bs-toggle="popover" title="Custom popover" data-bs-content="This is a Bootstrap popover.">Example popover</a>
|
|
||||||
|
|
||||||
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasExample" aria-labelledby="offcanvasExampleLabel">
|
|
||||||
<div class="offcanvas-header">
|
|
||||||
<h5 class="offcanvas-title" id="offcanvasExampleLabel">Offcanvas</h5>
|
|
||||||
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="offcanvas-body">
|
|
||||||
<div>
|
|
||||||
Some text as placeholder. In real life you can have the elements you have chosen. Like, text, images, lists, etc.
|
|
||||||
</div>
|
|
||||||
<div class="dropdown mt-3">
|
|
||||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown">
|
|
||||||
Dropdown button
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
|
||||||
<li><a class="dropdown-item" href="#">Action</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#">Another action</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#">Something else here</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="col-1 my-5 mx-0">
|
|
||||||
|
|
||||||
<h2>Guides</h2>
|
|
||||||
<p>Read more detailed instructions and documentation on using or contributing to Bootstrap.</p>
|
|
||||||
<ul class="icon-list">
|
|
||||||
<li><a href="https://getbootstrap.com/docs/5.2/getting-started/introduction/">Bootstrap quick start guide</a></li>
|
|
||||||
<li><a href="https://getbootstrap.com/docs/5.2/getting-started/webpack/">Bootstrap Webpack guide</a></li>
|
|
||||||
<li><a href="https://getbootstrap.com/docs/5.2/getting-started/parcel/">Bootstrap Parcel guide</a></li>
|
|
||||||
<li><a href="https://getbootstrap.com/docs/5.2/getting-started/vite/">Bootstrap Vite guide</a></li>
|
|
||||||
<li><a href="https://getbootstrap.com/docs/5.2/getting-started/build-tools/">Contributing to Bootstrap</a></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<hr class="mt-5 mb-4">
|
|
||||||
|
|
||||||
<p class="text-muted">Created and open sourced by the Bootstrap team. Licensed MIT.</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
Loading…
Reference in New Issue
Block a user