diff --git a/CHANGELOG.md b/CHANGELOG.md index f88b317..6fb6577 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, diff --git a/custom-panel.html b/custom-panel.html new file mode 100644 index 0000000..604b1a0 --- /dev/null +++ b/custom-panel.html @@ -0,0 +1,37 @@ + + + + + + + Slide-Deck Web Component Demo + + + + + + + +
+ + +
+
+
+
+

Slide-Deck Web Component

+

+ github.com/oddbird/slide-deck/ +

+
+
+

Custom Control Panel

+

command-k to open

+
+
+ Back to demo +
+
+ + + diff --git a/index.html b/index.html index 766831b..c78a88a 100644 --- a/index.html +++ b/index.html @@ -1,11 +1,12 @@ - + Slide-Deck Web Component Demo - + + @@ -92,6 +93,10 @@

<button set-view>list<button>

+
+

Custom Control Panel

+ Example +

Speaker View

Open Source

diff --git a/slide-deck.css b/slide-deck.css new file mode 100644 index 0000000..3c2c2da --- /dev/null +++ b/slide-deck.css @@ -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; + } +} diff --git a/slide-deck.js b/slide-deck.js index 17cb295..5681a82 100644 --- a/slide-deck.js +++ b/slide-deck.js @@ -3,122 +3,44 @@ class slideDeck extends HTMLElement { static appendShadowTemplate = (node) => { const template = document.createElement("template"); template.innerHTML = ` - -
-
- - - + + + +
+ Slide-Deck Controls +
+
+
+ + +

Presentation:

- - - + + + + +

View:

- - - -
-
-
- -
+ + + +
+
+ `; 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', @@ -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])'); + this.slideCount = slides.length; this.defaultAttrs(); - this.setSlideIDs(); + this.setSlideIDs(slides); this.goTo(); // buttons @@ -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); }); @@ -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) => { @@ -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]); } }); } @@ -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); }); }