@ou-imdt/css
Version:
The IMDT CSS library styles native elements with light, extendable CSS. It is developed for Interactive Media Developers at the Open University.
488 lines (397 loc) • 17.8 kB
Markdown
# Layout Intro
Layout utility classes have been added to CSS to make developing widgets more consistent. _It should be kept in mind these utilities will not cover all situations._
The tool can be used to select options to better suit the situation and generate basic markup that, when used with imd css, will give a basic widget layout.
_It's recomended interactives have at the least a widget and feedback region._
<style>
[data-layout] {
border: 2px solid var(--accent2-light);
padding: 1ch;
border-radius: 5px;
}
/* Copy for use outside of layout */
.options {
display: flex;
gap: 1.5rem;
flex-wrap: wrap;
}
</style>
<fieldset id="preset-options">
<legend>Layout Presets</legend>
<p>Click to see the setup for the following variations.</p>
<div class="grid">
<button data-preset="minimal">Minimal</button>
<button data-preset="simple">Simple</button>
<button data-preset="complex">Complex</button>
</div>
</fieldset>
<fieldset class="grid" id="granular-options">
<legend>Granular options</legend>
<label for="help">
<input id="help" type="checkbox" data-variation="help"> Help
</label>
<label for="simple-menu">
<input id="simple-menu" type="checkbox" data-variation="simpleMenu"> Simple menu
</label>
<label for="complex-menu">
<input id="complex-menu" type="checkbox" data-variation="complexMenu"> Complex menu
</label>
<label for="controls">
<input id="controls" type="checkbox" data-variation="controls">Controls
</label>
</fieldset>
<fieldset>
<legend>Dev</legend>
<div class="grid">
<button id="markup">Copy Markup</button>
<small>A simplified version of this is available in the Markup section below.</small>
</div>
<details class="accent4">
<summary>Demo Settings</summary>
<p>You can use demo settings to simulate different feedback messages for different scenarios.</p>
<form class="" id="options-area">
<fieldset class="options">
<legend>Basic message options</legend>
<label><input type="checkbox" data-option="loadData"></input> Load data</label>
<label><input class="accent1 fg" type="checkbox" data-option="loadErr"></input> Load error</label>
<label><input type="checkbox" data-option="loadNull"></input> Load no data</label>
<label><input type="checkbox" data-option="resetErr"></input> Reset error</label>
<label><input type="checkbox" data-option="saveErr"></input> Save error</label>
<label><input type="checkbox" data-option="revealErr"></input> Reveal error</label>
</fieldset>
<p class="information sem-icon">
Only the Load error settings are persistent through page refresh to enable viewing of load messages.
</p>
<p>
<small>Be aware multiple enabled load options will display with the precedence (error, no data, data) with error
being the highest.</small>
</p>
<fieldset class="options">
<legend>Question checking options</legend>
<label><input type="checkbox" data-option="checkErr"></input> Check error</label>
<label><input type="checkbox" data-option="checkWrong"></input> Answer wrong</label>
<label><input type="checkbox" data-option="checkPlural"></input> Plural</label>
</fieldset>
<fieldset>
<legend for="revealTerm">The term used in the reveal message</legend>
<select data-option="revealTerm" name="revealTerm" id="revealTerm">
<option value="discussion">discussion</option>
<option value="result">result</option>
<option value="feedback">feedback</option>
</select>
</fieldset>
<fieldset class="options">
<legend>Check type</legend>
<label><input type="radio" name="check" data-option="checkGeneral"></input> Check general</label>
<label><input type="radio" name="check" data-option="checkPrecise"></input> Check precise</label>
<label><input type="radio" name="check" data-option="checkPartial"></input> Check partial</label>
</fieldset>
</form>
</details>
</fieldset>
<div data-layout="basic">
<details class="help icon-end">
<summary class="information">Help</summary>
<div>
<p>This is where any help text or accessibility information should be.<p>
</div>
</details>
<details class="menu accent1 border-color">
<summary class="accent1 bg-color">Complex Menu</summary>
Any advanced configuration or menu controls can be nested here. Otherwise the Simple menu may be more suitable.
</details>
<ul role="list" class="menu accent1 bg-color unstyled">
<li>Simple Menu</li>
<li>
<button aria-label="Add new item to widget" class="accent1" onclick="alert(`I don't do anything`)">New +</button>
</li>
</ul>
<div class="widget neutral border">Widget Box</div>
<div class="feedback sem-icon" data-feedback aria-live="polite" aria-atomic="true">
</div>
<div class="controls">
<ul>
<li><button class="accent1 bg-color" data-control="reset">Reset</button></li>
<li><button class="accent1 bg-color" data-control="save">Save</button></li>
<li><button data-control="check">Check</button></li>
<li><button data-control="reveal">Reveal</button></li>
</ul>
</div>
<div class="reveal information border" data-reveal aria-hidden tabindex="-1">
<h2>Revealed content</h2>
This is where any extensive content or results should be presented.
</div>
</div>
<script type="module">
// Script to handle simulated control and message behaviours
const feedbackClasses = ["neutral", "information", "success", "warning", "danger"];
const messages = {
load: "Your saved activity has been loaded.",
loadNull: "No previously saved activity. ",
loadErr: "A problem has occurred, load failed.",
save: "Your activity has been saved.",
saveErr: "A problem has occurred, saving failed.",
reset: "Your activity has been reset.",
resetErr: "A problem has occurred, reset failed.",
checkPrecise: (correctCount, total) => `You got ${correctCount} out of ${total} correct.`,
checkSingular: (correct) => correct ? 'Your answer is correct' : 'Your answer is incorrect.',
checkPlural: (correct) => correct ? 'Your answers are correct' : 'Your answers are incorrect.',
checkPartial: (plural) => plural ? 'Your answers are partially correct' : 'Your answer is partially correct',
checkErr: "A problem has occurred, checking failed.",
reveal: (type="answer", single= true) => `The ${type} ${single ? 'has': 'have'} been revealed.`,
revealErr: "A problem has occurred, reveal failed. "
}
const optionsArea = document.getElementById('options-area');
const optionElements = Array.from(optionsArea.querySelectorAll('[data-option]'));
const options = optionElements.reduce((acc, cur) => {
const key = cur.getAttribute('data-option');
if(cur.type === "checkbox") {
cur.checked = localStorage.getItem(key) ?? false;
} else {
cur.value = localStorage.getItem(key) ?? false;
}
if(key === "checkGeneral") cur.checked = true;
acc[key] = cur;
return acc;
}, {});
const layout = document.querySelector('[data-layout="basic"]');
const controls = Array.from(document.querySelectorAll('[data-control]'));
const revealer = document.querySelector('[data-reveal]');
const feedback = document.querySelector('[data-feedback]');
const updateFeedback = (message, semanticClass = "information") => {
feedback.classList.remove(...feedbackClasses);
feedback.classList.add(semanticClass);
feedback.innerHTML = message;
}
const actions = {
load: () => {
if(options.loadErr.checked) return updateFeedback(messages.loadErr, "danger");
if(options.loadNull.checked) return updateFeedback(messages.loadNull, "information");
if(options.loadData.checked) return updateFeedback(messages.load, "success");
},
reset: () => {
if(options.resetErr.checked) return updateFeedback(messages.resetErr, "danger");
updateFeedback(messages.reset, "success");
revealer.setAttribute('aria-hidden', "true");
revealer.setAttribute('tabindex', -1);
},
save: () => {
if(options.saveErr.checked) return updateFeedback(messages.saveErr, "danger");
updateFeedback(messages.save, "success");
},
check: () => {
const term = options.revealTerm.value;
const isPlural = options.checkPlural.checked;
const isWrong = options.checkWrong.checked;
const semanticClass = isWrong ? 'warning' : 'success';
if(options.checkErr.checked) return updateFeedback(messages.checkErr, "danger");
if(options.checkPrecise.checked) {
const correctCount = isWrong ? 5 : 10;
return updateFeedback(messages.checkPrecise(correctCount, 10), semanticClass);
}
if(options.checkPartial.checked) {
return updateFeedback(messages.checkPartial(isPlural), "warning");
}
const correctMessage = isPlural ? messages.checkPlural(true) : messages.checkSingular(true);
const wrongMessage = isPlural ? messages.checkPlural(false) : messages.checkSingular(false);
if(isWrong) {
return updateFeedback(wrongMessage, semanticClass);
}
return updateFeedback(correctMessage, semanticClass);
},
reveal: () => {
if(options.revealErr.checked) return updateFeedback(messages.revealErr, "danger");
const term = options.revealTerm.value || 'answer';
const single = term !== "answers"
updateFeedback(messages.reveal(term, single), "success");
revealer.removeAttribute('aria-hidden');
revealer.removeAttribute('tabindex');
}
};
// fire relevant option when control button is clicked
controls.map(control => {
const key = control.getAttribute('data-control');
control.addEventListener('click', (e) => actions[key]());
});
// update localStorage on input change
Object.values(options).map(el => {
const key = el.getAttribute('data-option');
el.addEventListener('change', () => {
if(!['loadErr', 'loadNull', 'loadData'].includes(key)) return;
el.checked ? localStorage.setItem(key, true) : localStorage.removeItem(key);
})
});
actions.load();
options.revealTerm.selectedIndex = 0;
</script>
<script type="module">
// Script to configure layout options
const layout = document.querySelector('[data-layout="basic"]');
const parts = [
{key: 'help', el: layout.querySelector('.help')},
{key: 'simpleMenu', el: layout.querySelector('.menu:not(details)')},
{key: 'complexMenu', el: layout.querySelector('details.menu')},
{key: 'widget', el: layout.querySelector('.widget')},
{key: 'feedback', el: layout.querySelector('.feedback')},
{key: 'controls', el: layout.querySelector('.controls')},
{key: 'reveal', el: layout.querySelector('.reveal')}
];
// group checkbox option with part data
const variationOptions = Array.from(document.querySelectorAll('[data-variation]'));
variationOptions.map(option => {
const key = option.getAttribute('data-variation');
if(key) {
const part = parts.find(i => i.key === key);
part.option = option;
}
});
const resetVariation = (keys = []) => {
parts.map(part => {
if(part.option) {
part.option.checked = keys.includes(part.key) ? true : false;
}
});
};
// Updates the preset button based on selected granular options
const checkIfPresetActive = () => {
const minimal = document.querySelector("button[data-preset='minimal']");
const simple = document.querySelector("button[data-preset='simple']");
const complex = document.querySelector("button[data-preset='complex']");
const activeParts = parts.filter(i => i.option?.checked).map(i => i.key);
[minimal, simple, complex].map(i => i.setAttribute('aria-pressed', false));
const isMinimal = activeParts.length === 0;
const isSimple = activeParts.join('') === ['help', 'simpleMenu'].join('');
const isComplex = activeParts.join('') === ['help', 'complexMenu', 'controls'].join('');
if(isMinimal) minimal.setAttribute('aria-pressed', true);
if(isSimple) simple.setAttribute('aria-pressed', true);
if(isComplex) complex.setAttribute('aria-pressed', true);
}
const renderDemo = () => {
layout.replaceChildren();
parts.map(({key, option, el}) => {
// parts without options are required so are always added
if(!option || option.checked === true) {
return layout.appendChild(el);
}
});
// but we should reset reveal if controls are disabled
const controls = parts.find(i => i.key === "controls");
const reveal = parts.find(i => i.key === "reveal");
if(controls.option.checked === false) {
reveal.el.setAttribute('aria-hidden', true);
reveal.el.setAttribute('tabindex', -1);
}
checkIfPresetActive()
}
document.querySelector('fieldset#granular-options').addEventListener('change', ({target}) => {
if(target.tagName === 'INPUT') {
renderDemo();
}
});
document.querySelector('fieldset#preset-options').addEventListener('click', ({target}) => {
if(target.tagName === 'BUTTON') {
const preset = target.getAttribute('data-preset');
switch(preset) {
case 'minimal':
resetVariation();
break;
case 'simple':
resetVariation(['simpleMenu', 'help']);
break;
case 'complex':
resetVariation(['complexMenu', 'help', 'controls', 'reveal']);
break
}
renderDemo();
}
});
renderDemo();
</script>
<script type="module">
const copyMarkup = document.getElementById('markup');
const layout = document.querySelector('[data-layout="basic"]');
copyMarkup.addEventListener('click', () =>{
navigator.clipboard.writeText(layout.outerHTML);
alert('current markup copied');
});
</script>
## Feedback
Please refer to the semantic palette for detailed information on semantic colours and icons, but for the sake of quick reference the '.feedback' element has a semantic class applied by default. Changing that class to one of 'information, success, warning, danger, neutral' will change the appearance.
Remove the semantic class to remove the styling.
If you want to prefix your feedback messages with a visual icon apply the class `.sem-icon` alongside a semantic class and the matching icon will be applied. This has been applied to the markup generated by default.
<details>
<summary>Markup</summary>
## Markup
HTML examples of the presets are available below if there are issues using the 'Copy Markup' function (it doesn't preserve indentation perfectly).
Feel free to copy, paste and edit as needed.
__Usage__
- The classes identifying sections 'widget', 'feedback', 'help', 'menu' and 'reveal' MUST be nested inside an element with the attribute 'data-layout="basic"'.
- The feedback class has a minimum height enforced to prevent reflow when when it is populated.
- Though the preferred order has been applied in examples elements can be re-ordered as needed.
- As with CSS as a whole, it is expected that developers will override styles and markup as they need to for their use-case.
- Feel free to add IDs to items if needed
## Minimal
```html
<div data-layout="basic">
<div class="widget neutral border">Widget Box</div>
<div class="feedback sem-icon danger" data-feedback="" aria-live="polite" aria-atomic="true">
A problem has occurred, load failed.
</div>
</div>
```
## Simple
```html
<div data-layout="basic">
<details class="help">
<summary class="information">Help</summary>
<div>
<p>This is where any help text or accessibility information should be.</p><p>
</p></div>
</details>
<ul class="menu accent1 bg-color unstyled">
<li>Simple Menu</li>
<li>
<button aria-label="Add new item to widget" class="accent1" onclick="alert(`I don't do anything`)">New +</button>
</li>
</ul>
<div class="widget neutral border">Widget Box</div>
<div class="feedback sem-icon danger" data-feedback="" aria-live="polite" aria-atomic="true">
A problem has occurred, load failed.
</div>
<div class="reveal information border" data-reveal="" aria-hidden="true" tabindex="-1">
<h2>Revealed content</h2>
This is where any extensive content or results should be presented.
</div>
</div>
```
## Complex
```html
<div data-layout="basic">
<details class="help">
<summary class="information">Help</summary>
<div>
<p>This is where any help text or accessibility information should be.</p>
</div>
</details>
<details class="menu accent1 border-color">
<summary class="accent1 bg-color">Complex Menu</summary>
Any advanced configuration or menu controls can be nested here. Otherwise the Simple menu may be more suitable.
</details>
<div class="widget neutral border">Widget Box</div>
<div class="feedback sem-icon danger" data-feedback="" aria-live="polite" aria-atomic="true">
A problem has occurred, load failed.
</div>
<div class="controls">
<ul>
<li><button class="accent1 bg-color" data-control="reset">Reset</button></li>
<li><button class="accent1 bg-color" data-control="save">Save</button></li>
<li><button data-control="check">Check</button></li>
<li><button data-control="reveal">Reveal</button></li>
</ul>
</div>
<div class="reveal information border" data-reveal="" aria-hidden="true" tabindex="-1">
<h2>Revealed content</h2>
This is where any extensive content or results should be presented.
</div>
</div>
```
</details>