Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Control panel styles and parts #10

Merged
merged 6 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ until we achieve a stable v1.0 release

## v0.1.2 - unreleased

- 💥 BREAKING: Removed the shadow DOM content wrapper,
and all shadow DOM styles
- 💥 BREAKING: Renamed and added control-panel parts,
to allow for more customization of the default panel
- 🚀 NEW: Default styles are in `slide-deck.css`
and can be applied from the light DOM
- 🚀 NEW: The entire control panel can be replaced
from the light DOM using `slot=control-panel`
on a slotted `dialog` element
- 🐞 FIXED: Slotted controls are no longer treated as slides
- 🚀 NEW: When `key-control` is activated (including on-load),
we target the stored active slide (or the first slide)
- 🐞 FIXED: When restoring the active slide from memory,
Expand Down
37 changes: 37 additions & 0 deletions custom-panel.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en" style="color-scheme: light dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Demo of slide-deck Web Component" />
<title>Slide-Deck Web Component Demo</title>
<link rel="stylesheet" href="slide-deck.css">
<style>body { margin: 0; }</style>
<script type="module" src="slide-deck.js"></script>
</head>
<body>
<slide-deck id="my-slides" key-control>
<dialog slot="control-panel">
<div>
<button set-view>grid</button>
<button set-view>list</button>
<form method="dialog"><button>close</button></form>
</div>
</dialog>
<header>
<h1>Slide-Deck Web Component</h1>
<p>
<a href="https://github.com/oddbird/slide-deck/">github.com/oddbird/slide-deck/</a>
</p>
</header>
<div>
<h2>Custom Control Panel</h2>
<p><strong>command-k</strong> to open</p>
</div>
<div>
<a href="./index.html">Back to demo</a>
</div>
</slide-deck>

</body>
</html>
9 changes: 7 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" style="color-scheme: light dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Demo of slide-deck Web Component" />
<title>Slide-Deck Web Component Demo</title>

<link rel="stylesheet" href="slide-deck.css">
<style>body { margin: 0; }</style>
<script type="module" src="slide-deck.js"></script>
</head>
<body>
Expand Down Expand Up @@ -92,6 +93,10 @@ <h2><code>&lt;button set-view&gt;list&lt;button&gt;</code></h2>
<button set-view>grid</button>
</div>
</div>
<div>
<h2>Custom Control Panel</h2>
<a href="./custom-panel.html">Example</a>
</div>
<div><h2>Speaker View</h2></div>
<div>
<h2>Open Source</h2>
Expand Down
73 changes: 73 additions & 0 deletions slide-deck.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
slide-deck {
--gap: clamp(8px, 0.25em + 1vw, 16px);
container: slide-deck / inline-size;
display: grid;

&[slide-view=grid] {
--slide-ratio: 16/9;
--target-outline: medium dotted;
--target-margin: var(--gap);
gap: var(--gap);
grid-template-columns: repeat(auto-fill, minmax(min(50ch, 100%), 1fr));
padding: var(--gap);
}

&[slide-view=list] {
--slide-height: 100svh;
}

&[blank-slide]::after {
content: '';
background-color: var(--blank-color, black);
position: absolute;
inset: 0;
}

&[blank-slide=white] {
--blank-color: white;
}

[id^=slide_] {
aspect-ratio: var(--slide-ratio);
border: thin solid;
box-sizing: border-box;
container: slide-item / inline-size;
min-height: var(--slide-height);
padding: var(--gap);
scroll-margin-block: var(--target-margin);

&:target {
outline: var(--target-outline);
outline-offset: calc(var(--gap) * 0.5);
}
}

&::part(control-panel) {
min-width: min(50ch, 100%);
padding: 0;
}

&::part(panel-header) {
border-block-end: thin solid;
display: grid;
gap: var(--gap);
grid-template-columns: 1fr auto;
padding: var(--gap);
}

&::part(controls) {
padding: var(--gap);
}

button,
&::part(button) {
font: inherit;
padding-inline: var(--gap);
border: medium solid transparent;
}

[aria-pressed=true],
&::part(button pressed) {
border-color: currentColor;
}
}
173 changes: 56 additions & 117 deletions slide-deck.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,122 +3,44 @@ class slideDeck extends HTMLElement {
static appendShadowTemplate = (node) => {
const template = document.createElement("template");
template.innerHTML = `
<dialog part="controls">
<form method="dialog"><button>close</button></form>
<div>
<slot name="slide-controls">
<button slide-event='toggleControl'>keyboard controls</button>

<slot></slot>
<slot name="control-panel">
<dialog part="control-panel">
<div part="panel-header">
<strong>Slide-Deck Controls</strong>
<form method="dialog"><button>close</button></form>
</div>
<div part="controls">
<button part="button" slide-event='toggleControl'>
keyboard navigation
</button>

<hr>
<p><strong>Presentation:</strong></p>
<button slide-event>start</button>
<button slide-event>end</button>
<button slide-event="joinWithNotes">speaker view</button>

<button part="button event" slide-event>
start
</button>
<button part="button event" slide-event>
end
</button>
<button part="button event" slide-event="joinWithNotes">
speaker view
</button>

<hr>
<p><strong>View:</strong></p>
<button set-view>grid</button>
<button set-view>list</button>
</slot>
</div>
</dialog>
<div part="contents">
<slot></slot>
</div>

<button part="button view" set-view>grid</button>
<button part="button view" set-view>list</button>
</div>
</dialog>
</slot>
`;
const shadowRoot = node.attachShadow({ mode: "open" });
shadowRoot.appendChild(template.content.cloneNode(true));
}

// css
static adoptShadowStyles = (node) => {
const shadowStyle = new CSSStyleSheet();
shadowStyle.replaceSync(`
:host {
position: relative;
}

:host:not(:fullscreen) {
container: host / inline-size;
}

:host(:fullscreen) {
background-color: white;
overflow-x: clip;
overflow-y: auto;
}

:host([slide-view=grid]) {
---slide-grid-ratio: 16/9;
---slide-grid-border: var(--slide-grid-border, thin solid);
---slide-grid-active-outline: medium dotted hotpink;
---slide-grid-scroll-margin: clamp(10px, 4cqi, 40px);
---slide-list-border: var(--slide-grid-border, thin solid);
}

:host([slide-view=list]) {
---slide-list-border: var(--slide-list-border, thin solid);
}

:host([blank-slide])::after {
content: '';
position: absolute;
inset: 0;
background-color: var(--blank-slide-color, black);
}

:host([blank-slide='white'])::after {
--blank-slide-color: white;
}

[part=contents] {
---slide-gap: clamp(5px, 1.5cqi, 15px);
display: grid;

:host([slide-view=grid]) & {
grid-template-columns: var(
--slide-grid-columns,
repeat(auto-fit, minmax(min(50ch, 100%), 1fr))
);
gap: var(--slide-grid-gap, var(---slide-gap));
padding: var(--slide-grid-padding, var(---slide-gap));
}

:host([slide-view=list]) & {
grid-auto-rows: var(--slide-list-rows, 100svh);
}
}

::slotted([id^=slide_]) {
aspect-ratio: var(--slide-grid-ratio, var(---slide-grid-ratio));
container-name: slide;
container-type: var(--slide-container, inline-size);
border: var(---slide-grid-border);
border-block-end: var(---slide-list-border);
padding: var(---slide-gap);
scroll-margin: var(
--slide-grid-scroll-margin,
var(---slide-grid-scroll-margin)
);
}

::slotted([id^=slide_]:target) {
outline: var(
--slide-grid-active-outline,
var(---slide-grid-active-outline)
);
outline-offset: var(--slide-active-outline-offset, 3px);
}

button[aria-pressed=true] {
box-shadow: inset 0 0 2px black;

&::before {
content: ' ✅ ';
}
}
`);
node.shadowRoot.adoptedStyleSheets = [shadowStyle];
}

// static
static observedAttributes = [
'key-control',
Expand Down Expand Up @@ -210,17 +132,18 @@ class slideDeck extends HTMLElement {

// shadow dom and ID
slideDeck.appendShadowTemplate(this);
slideDeck.adoptShadowStyles(this);
this.setDeckID();

// relevant nodes
this.body = document.querySelector('body');
this.controlPanel = this.shadowRoot.querySelector(`[part="controls"]`);
this.controlPanel = this.querySelector(`[slot="control-panel"]`) ??
this.shadowRoot.querySelector(`[part="control-panel"]`);

// initial setup
this.slideCount = this.childElementCount;
const slides = this.querySelectorAll(':scope > :not([slot])');
mirisuzanne marked this conversation as resolved.
Show resolved Hide resolved
this.slideCount = slides.length;
this.defaultAttrs();
this.setSlideIDs();
this.setSlideIDs(slides);
this.goTo();

// buttons
Expand Down Expand Up @@ -289,9 +212,7 @@ class slideDeck extends HTMLElement {

slideId = (n) => `slide_${this.id}-${n}`;

setSlideIDs = () => {
const slides = this.querySelectorAll(':scope > *');

setSlideIDs = (slides) => {
slides.forEach((slide, index) => {
slide.id = this.slideId(index + 1);
});
Expand All @@ -309,6 +230,24 @@ class slideDeck extends HTMLElement {

// buttons
getButtonEvent = (btn) => btn.getAttribute('slide-event') || btn.innerText;
setPressed = (btn, isPressed) => {
btn.setAttribute('aria-pressed', isPressed);

if (btn.hasAttribute('part')) {
const currentNames = btn.getAttribute('part').split(' ');
let newNames;

if (isPressed) {
newNames = currentNames.includes('pressed')
? currentNames
: [...currentNames, 'pressed'];
} else if (!isPressed) {
newNames = currentNames.filter((name) => name !== 'pressed')
}

btn.setAttribute('part', newNames.join(' '));
}
}

updateEventButtons = () => {
this.eventButtons.forEach((btn) => {
Expand All @@ -320,7 +259,7 @@ class slideDeck extends HTMLElement {
}

if (Object.keys(isActive).includes(btnEvent)) {
btn.setAttribute('aria-pressed', isActive[btnEvent]);
this.setPressed(btn, isActive[btnEvent]);
}
});
}
Expand Down Expand Up @@ -362,7 +301,7 @@ class slideDeck extends HTMLElement {
updateViewButtons = () => {
this.viewButtons.forEach((btn) => {
const isActive = this.getButtonView(btn) === this.slideView;
btn.setAttribute('aria-pressed', isActive);
this.setPressed(btn, isActive);
});
}

Expand Down