First draft done

This commit is contained in:
Lewis Dale 2023-10-02 09:14:19 +01:00
parent 2fc934e324
commit 85bf38b5d5
13 changed files with 807 additions and 12 deletions

View File

@ -7,6 +7,8 @@ module.exports = function (config) {
config.addPlugin(syntaxHighlight);
config.addPlugin(markdown);
config.addPassthroughCopy("src/assets");
return {
passthroughFileCopy: true,
dataTemplateEngine: false,

View File

@ -10,4 +10,6 @@
</head>
<body>
{{ content | safe }}
<script type="text/javascript" src="/assets/js/react-examples.js" defer></script>
</body>

231
src/assets/js/react-examples.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,9 @@
## What do you mean?
---
type: statement
---
## Huh?
* When we create a custom component, often we have to tell the browser how it should treat it
* But often browsers already give us the tools to do that natively
* Reaching for CSS and HTML first can make our components faster, more reusable, and more accessible
* Reaching for CSS and HTML first can make our components faster, more reusable, and more accessible

View File

@ -147,6 +147,8 @@ export default class Toggle extends Component<Props, State> {
<div>
<div data-react-root="toggle"></div>
* 139 lines of code (+ the Icons, theme, 3 libraries required)
* Manual state management
* Have to import all of this everywhere we use it

191
src/slides/05-example-2.md Normal file
View File

@ -0,0 +1,191 @@
## Example 2: Modals
<div class="grid">
```typescript
import React, { MouseEventHandler, PureComponent, ReactNode } from 'react';
import ReactDOM from 'react-dom';
import styled, { css } from 'styled-components';
import { H4 } from '../Typography';
import Icon from 'components/Icon';
import isEventEscapeKey from 'helpers/isEventEscapeKey';
import theme from 'theme';
export const CloseIcon = styled(Icon).attrs({
size: 20,
})`
color: ${theme.colours.primary500};
transition: color 0.3s ease;
position: absolute;
right: 30px;
top: 30px;
z-index: 1;
&[disabled] {
opacity: 0.6;
}
&:not([disabled]) {
cursor: pointer;
}
`;
export const ModalOverlay = styled.div`
top: 0;
left: 0;
position: fixed;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
z-index: 1000;
background-color: rgba(0, 0, 0, 0.4);
`;
export const ModalContent = styled.div<StyleProps>`
min-width: 500px;
background-color: white;
border-radius: 4px;
padding: 30px;
position: relative;
line-height: 1.43;
max-height: 80%;
${({ scrollable }) =>
scrollable
? css`
overflow-y: auto;
`
: ''};
`;
export type StyleProps = {
scrollable?: boolean;
};
export type Props = StyleProps & {
children: ReactNode;
onClose: MouseEventHandler<HTMLDivElement | HTMLButtonElement>;
className?: string;
disabled?: boolean;
};
class ModalContainer extends PureComponent<Props> {
componentDidMount() {
document.addEventListener('keydown', this.onEscapePress);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.onEscapePress);
}
overlayClicked: MouseEventHandler<HTMLDivElement> = (e) => {
e.stopPropagation();
if (!this.props.disabled && e.target === e.currentTarget) {
this.props.onClose(e);
}
};
onEscapePress: EventListenerOrEventListenerObject = (e) => {
e.stopPropagation();
if (!this.props.disabled && isEventEscapeKey(e as KeyboardEvent)) {
this.props.onClose(e as any);
}
};
onClose: MouseEventHandler<HTMLDivElement> = (e) => {
e.stopPropagation();
if (!this.props.disabled) {
this.props.onClose(e);
}
};
render() {
const modal = (
<ModalOverlay onClick={this.overlayClicked}>
<ModalContent className={this.props.className} scrollable={this.props.scrollable}>
<CloseIcon onClick={this.onClose} icon="close" />
{this.props.children}
</ModalContent>
</ModalOverlay>
);
// defaulting to document.body mostly for the benefit of tests, which don't have a #app-container element.
const rootElement = document.getElementById('app-container') || document.body;
return ReactDOM.createPortal(modal, rootElement);
}
}
export const ModalContents = styled.div`
display: flex;
flex-direction: column;
`;
export const ModalHeader = styled(H4)`
margin-bottom: 15px;
`;
export const ModalBody = styled.div`
font-size: 14px;
line-height: 25px;
`;
export const ModalFooter = styled.div`
width: 100%;
display: flex;
justify-content: flex-end;
align-items: center;
margin-top: 15px;
& > {
margin-left: 20px;
}
`;
type Props = {
visible: boolean;
header: ReactNode;
body: ReactNode;
footer?: ReactNode;
loading?: boolean;
onClose: () => void;
};
class Modal extends PureComponent<Props> {
render() {
const { visible, header, body, footer, onClose, loading } = this.props;
if (!visible) {
return null;
}
return (
<ModalContainer onClose={onClose} disabled={loading} scrollable={true}>
<ModalContents>
<ModalHeader>{header}</ModalHeader>
<ModalBody>{body}</ModalBody>
<ModalFooter>{footer}</ModalFooter>
</ModalContents>
</ModalContainer>
);
}
}
```
<div>
<div data-react-root="modal"></div>
* 171 lines of code
* Requires state updates to display
* Separate component just to manage the backdrop
* How does a screen reader announce that the modal is displayed? (hint: it doesn't)
</div>
</div>

View File

@ -0,0 +1,72 @@
<h2>Example 2: Modals are just Dialogs</h2>
<div class="grid">
{% highlight "html" %}
<style>
dialog {
z-index: 10;
}
dialog::backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
</style>
<dialog id="displayDialog">
<form method="dialog">
<button autofocus>Close</button>
</form>
<h1>I'm a dialog!</h1>
</dialog>
<button onclick="showModal()">Open the dialog as a modal</button>
<button onclick="showDialog()">Open the dialog as a popup</button>
<script type="text/javascript">
const dialog = document.getElementById("displayDialog");
function showModal() {
dialog.showModal();
}
function showDialog() {
dialog.show();
}
</script>
{% endhighlight %}
<div>
<dialog id="displayDialog">
<form method="dialog">
<button autofocus>Close</button>
</form>
<h1>I'm a dialog!</h1>
</dialog>
<button onclick="showModal()">Open the dialog as a modal</button>
<button onclick="showDialog()">Open the dialog as a popup</button>
<ul>
<li>31 lines of code</li>
<li>A teensy bit of Javascript - I know, I'm a hypocrite</li>
<li>Option to use as a popup <i>or</i> a modal</li>
<li><code>::backdrop</code> pseudo-element for easy styling</li>
</ul>
<script type="text/javascript">
const dialog = document.getElementById("displayDialog");
function showModal() {
dialog.showModal();
}
function showDialog() {
dialog.show();
}
</script>
</div>
</div>

View File

@ -0,0 +1,86 @@
## Example 3: Accordions
<div class="grid">
```typescript
import React, { useState } from 'react';
import styled from 'styled-components';
import Icon from '../../../../../components/Icon';
import theme from '../../../../../theme';
type AccordionProps = {
title: string;
items: (string | null)[];
};
const Container = styled.div`
width: 100%;
`;
const AccordionSummary = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
cursor: pointer;
`;
interface ExpandedProps {
expanded: boolean;
}
export const StyledArrow = styled(Icon).attrs({
icon: 'dropdownArrow',
size: 15,
color: theme.colours.primary500,
})<ExpandedProps>`
position: relative;
transform: rotate(${({ expanded }) => (expanded ? '180deg' : '0')});
`;
const Title = styled.span<ExpandedProps>`
font-weight: ${({ expanded }) => (expanded ? 'bold' : 'normal')};
`;
const ItemList = styled.div`
display: flex;
flex-direction: column;
`;
const Accordion = ({ title, items }: AccordionProps) => {
const [expanded, setExpanded] = useState(false);
return (
<Container>
<AccordionSummary
onClick={() => {
setExpanded(!expanded);
}}
>
<Title expanded={expanded}>{title}</Title>
<StyledArrow expanded={expanded} />
</AccordionSummary>
{expanded && (
<ItemList>
{items.map((item) => {
return <div key={item}>{item}</div>;
})}
</ItemList>
)}
</Container>
);
};
export default Accordion;
```
<div>
<div data-react-root="accordion"></div>
* 67 lines of code
* Manual state management
* Divs with onClick handlers - unreachable by keyboard, no screen reader support
</div>
</div>

View File

@ -0,0 +1,91 @@
## Example 3: The discloure element
<div class="grid">
```html
<style>
details {
position: relative;
width: max-content;
min-width: 20ch;
background: white;
border-radius: 10px;
border: 1px solid darkgray;
}
summary {
display: flex;
cursor: pointer;
position: relative;
border-bottom: 1px solid transparent;
padding: 10px;
align-items: center;
justify-content: space-between;
}
summary svg {
width: 12px;
height: auto;
transform: rotate(180deg) translate(-1px);
transition: transform .3s linear;
}
summary::-webkit-details-marker {
display: none;
}
details[open] > summary {
border-color: darkgray;
}
details[open] summary svg {
transform: rotate(0deg);
}
</style>
<details>
<summary>Test Title <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 7">
<path
fill="none"
fillRule="evenodd"
stroke="currentColor"
strokeWidth="2"
d="M1 1l4.025 4L9 1.05"
/>
</svg>
</summary>
<ul>
<li>Test item 1</li>
<li>Test item 2</li>
</ul>
</details>
```
<div>
<details>
<summary>Test Title <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 7">
<path
fill="none"
fillRule="evenodd"
stroke="currentColor"
strokeWidth="2"
d="M1 1l4.025 4L9 1.05"
/>
</svg>
</summary>
<ul>
<li>Test item 1</li>
<li>Test item 2</li>
</ul>
</details>
</div>
</div>

View File

@ -0,0 +1,12 @@
---
type: statement
---
## Final points
* The minified, optimized React bundle I produced to showcase just _3_ components was 190kb before Gzipping
* The majority of that is React - 143kb
* This entire presentation, with lots of duplication, and every single slide included, is only 92kb
* When we build new components, let's see if the browser can't do it for us instead
* Modern CSS is great
* Javascript as a garnish, not a main course

View File

@ -0,0 +1,9 @@
---
type: statement
---
## Useful links
* W3C Aria Patterns - https://www.w3.org/WAI/ARIA/apg/patterns/
* Every Layout - https://every-layout.dev/
* Web.dev - https://web.dev/

View File

@ -7,6 +7,26 @@ body {
-webkit-overflow-scrolling: touch;
overflow: hidden;
font-family: Avenir, Montserrat, Corbel, 'URW Gothic', source-sans-pro, sans-serif;
}
::-webkit-scrollbar {
width: .5rem;
}
::-webkit-scrollbar-track {
background: transparent;
border: 1px solid transparent;
}
::-webkit-scrollbar-thumb {
background-color: var(--color-slate-800);
outline: 1px solid var(--color-slate-800);
border-radius: 10px;
}
::-webkit-scrollbar-corner {
background-color: transparent;
}
main {
@ -23,18 +43,11 @@ main {
.slide {
scroll-snap-align: start;
position: relative;
border-bottom: 1px solid black;
height: 100%;
flex: 1 0 100%;
font-size: var(--text-size-m);
&.title {
display: flex;
flex-direction: column;
gap: var(--space-size-sm);
align-items: center;
justify-content: center;
}
h1 {
font-size: var(--text-size-2xl);
@ -46,6 +59,25 @@ main {
padding-inline: var(--space-size-xs);
}
&.title {
display: flex;
flex-direction: column;
gap: var(--space-size-sm);
align-items: center;
justify-content: center;
h2 {
border-bottom: 0px solid;
}
}
&.statement {
ul {
width: 100%;
margin: var(--space-size-3xl) auto;
}
}
h1, h2, h3, h4 {
a {
text-decoration: none;
@ -83,9 +115,10 @@ main {
border-radius: 20px;
position: relative;
cursor: pointer;
&::before {
content: " ";
content: "";
width: 20px;
aspect-ratio: 1;
background: gray;
@ -113,7 +146,6 @@ main {
flex-direction: row;
flex-wrap: wrap;
align-items: space-around;
gap: var(--space-size-m);
padding: var(--space-size-s);
> :not(:first-child) {
@ -123,6 +155,7 @@ main {
flex-direction: column;
align-items: center;
padding: var(--space-size-s);
flex-basis: 40%;
}
> :first-child {
@ -131,5 +164,58 @@ main {
flex-grow: 1;
}
}
dialog {
z-index: 10;
&::backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
}
button {
margin-block-start: var(--space-size-s);
}
details {
position: relative;
width: max-content;
min-width: 20ch;
background: white;
border-radius: 10px;
border: 1px solid darkgray;
color: black;
summary {
display: flex;
cursor: pointer;
position: relative;
border-bottom: 1px solid transparent;
padding: 10px;
align-items: center;
justify-content: space-between;
svg {
width: var(--text-size-s);
height: auto;
transform: rotate(180deg) translate(-1px);
transition: transform .3s linear;
}
&::-webkit-details-marker {
display: none;
}
}
&[open] summary {
border-color: darkgray;
svg {
transform: rotate(0deg);
}
}
}
}