Skip to content

Commit

Permalink
Progressively enhance theme toggler: use system default when JS not a…
Browse files Browse the repository at this point in the history
…vailable
  • Loading branch information
florimondmanca committed Dec 19, 2023
1 parent f5d341c commit c714f8c
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 60 deletions.
9 changes: 5 additions & 4 deletions src/assets/customElements/eventForm.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @ts-check
import { onParsed } from '../lib/customElements';

export default class extends HTMLFormElement {
export default class extends HTMLElement {
connectedCallback() {
onParsed(() => {
const missionFields = document.getElementById('mission-fields');
Expand All @@ -10,16 +10,17 @@ export default class extends HTMLFormElement {
throw new Error('#mission-fields is missing');
}

const type = /** @type {HTMLSelectElement} */ (this.type);
const form = /** @type {HTMLFormElement} */ (this.querySelector('form'));
const type = /** @type {HTMLSelectElement} */ (form.type);

type.addEventListener('change', event => {
if (!event.target) {
return;
}
const isMission = event.target['value'] === 'mission';
missionFields.hidden = !isMission;
this.projectId.disabled = !isMission;
this.taskId.disabled = !isMission;
form.projectId.disabled = !isMission;
form.taskId.disabled = !isMission;
});
});
}
Expand Down
4 changes: 2 additions & 2 deletions src/assets/customElements/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import themeToggler from './themeToggler';

customElements.define('pc-auto-form', autoForm);
customElements.define('pc-clipboard-button', clipboardButton);
customElements.define('pc-event-form', eventForm, { extends: 'form' });
customElements.define('pc-event-form', eventForm);
customElements.define('pc-faircalendar-filters-form', fairCalendarFiltersForm);
customElements.define('pc-nav-menu-button', navMenuButton, {
extends: 'button'
});
customElements.define('pc-theme-toggler', themeToggler, { extends: 'button' });
customElements.define('pc-theme-toggler', themeToggler);
41 changes: 32 additions & 9 deletions src/assets/customElements/themeToggler.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
// @ts-check
import { createCookie } from '../lib/cookie';
import { createCookie, removeCookie } from '../lib/cookie';

export default class extends HTMLButtonElement {
export default class extends HTMLElement {
connectedCallback() {
let theme = document.documentElement.dataset.theme;
// Progressive enhancement: show toggle button only if this JavaScript loads.
const template = /** @type {HTMLTemplateElement} */ (this.querySelector(
'template'
));
this.appendChild(document.importNode(template.content, true));

if (!theme) {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)')
.matches;
theme = prefersDark ? 'dark' : 'light';
this._storeTheme(theme);
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');

let theme = this.dataset.theme; // Comes from the cookie

// Theme coming from the cookie has priority, use system default as a fallback.
if (theme) {
document.documentElement.setAttribute('data-theme', theme);
} else {
theme = prefersDark.matches ? 'dark' : 'light';
}

this.addEventListener('click', () => {
// Use new system default whenever it changes, until the user toggle the theme again.
prefersDark.addEventListener('change', () => {
theme = prefersDark.matches ? 'dark' : 'light';
this._clearStoredTheme();
});

// Toggle theme when user clicks on the button.
const toggleBtn = /** @type {HTMLButtonElement} */ (this.querySelector(
'button'
));
toggleBtn.addEventListener('click', () => {
theme = theme === 'dark' ? 'light' : 'dark';
this._storeTheme(theme);
});
Expand All @@ -27,4 +45,9 @@ export default class extends HTMLButtonElement {
// This will avoid FLOUC (Flashlight of unstyled content) when dark mode is used.
createCookie('theme', theme);
}

_clearStoredTheme() {
document.documentElement.removeAttribute('data-theme');
removeCookie('theme');
}
}
12 changes: 12 additions & 0 deletions src/assets/lib/cookie.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
// @ts-check
// Adapted from: http://www.quirksmode.org/js/cookies.html

/**
* @param {string} name
* @param {string} value
*/
export function createCookie(name, value) {
document.cookie = name + '=' + value + '; SameSite=Strict; path=/';
}

/**
* @param {string} name
*/
export function removeCookie(name) {
document.cookie = name + '=; SameSite=Strict; path=/';
}
3 changes: 3 additions & 0 deletions src/assets/styles/components/event.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pc-event-form {
display: block;
}
8 changes: 7 additions & 1 deletion src/assets/styles/components/icon.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
color: var(--text-action-violet);
}

[data-theme='dark'] .pc-icon--action-violet {
@media (prefers-color-scheme: dark) {
.pc-icon--action-violet {
color: var(--text-default);
}
}

html[data-theme='dark'] .pc-icon--action-violet {
color: var(--text-default);
}

Expand Down
1 change: 1 addition & 0 deletions src/assets/styles/components/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
@import './button.css';
@import './card.css';
@import './dropdown.css';
@import './event.css';
@import './form_errors.css';
@import './icon.css';
@import './input-group.css';
Expand Down
16 changes: 14 additions & 2 deletions src/assets/styles/components/theme.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
[data-theme='dark'] #theme-dark {
@media (prefers-color-scheme: dark) {
html:not([data-theme]) #theme-dark {
display: none;
}
}

@media (prefers-color-scheme: light) {
html:not([data-theme]) #theme-light {
display: none;
}
}

html[data-theme='dark'] #theme-dark {
display: none;
}

[data-theme='light'] #theme-light {
html[data-theme='light'] #theme-light {
display: none;
}
38 changes: 37 additions & 1 deletion src/assets/styles/variables.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,44 @@
--event-today: #fcf8e3;
}

[data-theme='dark'] {
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
/* Colors */
--background-default: #1a1b23;
--background-default-hover: #2a2b36;
--background-alt-grey: #121317;
--background-muted: #272930;
--background-muted-hover: #33353d;
--text-default: #ffffff;
--text-muted: #bbbbbb;
--border-default: #636363;
--text-action-violet-on-background-default: #cabffd;
/* FairCalendar */
--event-mission-background: rgb(4, 108, 78);
--event-mission-border: rgb(4, 108, 78);
--event-mission-text: var(--text-default);
--event-dojo-background: rgb(26, 86, 219);
--event-dojo-border: rgb(26, 86, 219);
--event-dojo-text: var(--text-default);
--event-support-background: rgb(81, 69, 205);
--event-support-border: rgb(81, 69, 205);
--event-support-text: var(--text-default);
--event-formationConference-background: rgb(180, 52, 3);
--event-formationConference-border: rgb(180, 52, 3);
--event-formationConference-text: var(--text-default);
--event-other-background: rgb(36, 38, 45);
--event-other-border: rgb(36, 38, 45);
--event-other-text: var(--text-default);
--event-holiday-background: rgb(159, 88, 10);
--event-holiday-border: rgb(159, 88, 10);
--event-holiday-text: var(--text-default);
--event-today: #645f44;
}
}

:root[data-theme='dark'] {
/* Colors */
/* COPY-PASTE OF THE ABOVE */
--background-default: #1a1b23;
--background-default-hover: #2a2b36;
--background-alt-grey: #121317;
Expand Down
12 changes: 8 additions & 4 deletions src/templates/components/theme_toggler.njk
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{% import 'macros/icons.njk' as icons %}

<button is="pc-theme-toggler" aria-hidden="true" class="pc-btn pc-btn--secondary pc-btn--circle">
{{ icons.sun({id: 'theme-light'}) }}
{{ icons.moon({id: 'theme-dark'}) }}
</button>
<pc-theme-toggler data-theme="{{ theme }}">
<template>
<button aria-hidden="true" class="pc-btn pc-btn--secondary pc-btn--circle">
{{ icons.sun({id: 'theme-light'}) }}
{{ icons.moon({id: 'theme-dark'}) }}
</button>
</template>
</pc-theme-toggler>
2 changes: 1 addition & 1 deletion src/templates/layouts/_base.njk
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="fr" {% if theme %}data-theme="{{ theme }}"{% endif %}>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
Expand Down
74 changes: 38 additions & 36 deletions src/templates/pages/faircalendar/events/_form.njk
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,52 @@

{% set isNotMission = event and event.type != 'mission' %}

<form method="POST" {{ render_attr(attr) }} is="pc-event-form">
<div class="pc-select-group">
<label class="pc-label required" for="type">{{ 'faircalendar-type-title'|trans }}</label>
<select name="type" id="type" required>
{% for type in types %}
<option value="{{ type }}"{% if event and event.type == type %}selected{% endif %}>{{ 'faircalendar-type-option'|trans({ type: type }) }}</option>
{% endfor %}
</select>
</div>

<div id="mission-fields" class="pc-grid pc-gap" style="--grid-cols: 2" {% if isNotMission %}hidden{% endif %}>
<pc-event-form>
<form method="POST" {{ render_attr(attr) }}>
<div class="pc-select-group">
<label class="pc-label required" for="projectId">{{ 'faircalendar-projectId-title'|trans }}</label>
<select name="projectId" id="projectId" required {% if isNotMission %}disabled{% endif %}>
{% for project in projects %}
<option value="{{ project.id }}" {% if event and event.project.id == project.id %}selected{% endif %}>{{ project.name }}</option>
<label class="pc-label required" for="type">{{ 'faircalendar-type-title'|trans }}</label>
<select name="type" id="type" required>
{% for type in types %}
<option value="{{ type }}"{% if event and event.type == type %}selected{% endif %}>{{ 'faircalendar-type-option'|trans({ type: type }) }}</option>
{% endfor %}
</select>
</div>

<div class="pc-select-group">
<label class="pc-label required" for="taskId">{{ 'faircalendar-taskId-title'|trans }}</label>
<select name="taskId" id="taskId" required {% if isNotMission %}disabled{% endif %}>
{% for task in tasks %}
<option value="{{ task.id }}" {% if event and event.task.id == task.id %}selected{% endif %}>{{ task.name }}</option>
{% endfor %}
</select>
<div id="mission-fields" class="pc-grid pc-gap" style="--grid-cols: 2" {% if isNotMission %}hidden{% endif %}>
<div class="pc-select-group">
<label class="pc-label required" for="projectId">{{ 'faircalendar-projectId-title'|trans }}</label>
<select name="projectId" id="projectId" required {% if isNotMission %}disabled{% endif %}>
{% for project in projects %}
<option value="{{ project.id }}" {% if event and event.project.id == project.id %}selected{% endif %}>{{ project.name }}</option>
{% endfor %}
</select>
</div>

<div class="pc-select-group">
<label class="pc-label required" for="taskId">{{ 'faircalendar-taskId-title'|trans }}</label>
<select name="taskId" id="taskId" required {% if isNotMission %}disabled{% endif %}>
{% for task in tasks %}
<option value="{{ task.id }}" {% if event and event.task.id == task.id %}selected{% endif %}>{{ task.name }}</option>
{% endfor %}
</select>
</div>
</div>
</div>

<div class="pc-grid pc-gap" style="--grid-cols: 2">
<div class="pc-select-group">
<label class="pc-label required" for="time">{{ 'faircalendar-time-title'|trans }}</label>
<select name="time" id="time" required>
{% for time in times %}
<option value="{{ time }}" {% if event and event.time == time %}selected{% endif %}>{{ time|minutesToHours }}</option>
{% endfor %}
</select>
<div class="pc-grid pc-gap" style="--grid-cols: 2">
<div class="pc-select-group">
<label class="pc-label required" for="time">{{ 'faircalendar-time-title'|trans }}</label>
<select name="time" id="time" required>
{% for time in times %}
<option value="{{ time }}" {% if event and event.time == time %}selected{% endif %}>{{ time|minutesToHours }}</option>
{% endfor %}
</select>
</div>
</div>
</div>

<input type="hidden" name="startDate" value="{{ startDate }}T00:00:00">
<input type="hidden" name="endDate" value="{{ endDate }}T00:00:00">
<input type="hidden" name="startDate" value="{{ startDate }}T00:00:00">
<input type="hidden" name="endDate" value="{{ endDate }}T00:00:00">

{{ buttons.save(attr={type: 'submit'}) }}
</form>
{{ buttons.save(attr={type: 'submit'}) }}
</form>
</pc-event-form>
{% endmacro %}

0 comments on commit c714f8c

Please sign in to comment.