Skip to content

front-end-foundations/Basilica

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

78 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Basilica!

Homework

Continue with the JS section on Front End Masters

Goals

  • Continue working with Flexbox and introduce CSS Grids
  • Review basic DOM manipulation techiques
  • Understand how to create an element and insert it into the DOM
  • Review GIT and Github set up and branching
  • Review NPM set up and installing

Initialize GIT with .gitignore

Download a zip file of this repo and cd into the directory.

Initialize a new git repository:

$ git init
$ git add .
$ git commit -m 'initial commit'

Note: the project is prepared for npm. It has a .gitignore file with the text node_modules in it.

NPM Initialization and Installs

Create a manifest (package.json) and install packages.

$ npm init
$ npm install browser-sync --save-dev

Review:

  • package.json
  • package-lock.json
  • hard dependencies vs devDependencies
  • node_modules folder
  • the need for a .gitignore file

Browser Sync CLI documentation

Add an npm command to the scripts section of package.json:

"scripts": {
  "start": "browser-sync app -w"
},

In VSCode's integrated terminal:

$ npm run start

Starter HTML

Image of Basilica

Open app/index.html in VSCode and examine the HTML with regards to the recipe schema. A schema is a semantic vocabulary of tags (or microdata) that you can add to your HTML to improve the way search engines read and represent your page in search engine result pages. There are many different kinds of schemas.

Note the itemscope attribute. There is a good example of using the recipe schema.

Demo: test the HTML with Rich Results Test and add any missing attributes.

There are other types of metadata that enrichness HTML. A popular one for sharing data is Open Graph Metadata.

Inspect the head of this recipe page and note the og: metadata.

Note the <abbr> tag.

Starter CSS

Examine the starter CSS.

Note:

  1. The pseudo class ::selection
  2. the use of max-width on the body selector
  3. the li > h4 child selector used to select elements with a specific parent. In this case it will select h4 tags only when the immediate parent is an li (compare this to li h4). Here's a complete listing of selector types in CSS.
  4. the css variables:
html {
  --basil-green: #88a308;
  --dark-gray: #333333;
  --light-gray: #e4e1d1;
  --light-green: #f5faef;
  --orange: #f90;
  --light-orange: #ebbd4e;
  --red: #f00;
  --max-width: 840px;
  --radius: 4px;
}

CSS variables are defined at a high level in the CSS (here the html selector is used ) to ensure that all the elements inherit and can use them.

CSS variables are applied as follows:

p {
  color: var(--basil-green);
}

Note also: the transition property on the anchors. transition: color 0.2s linear; is a shortcut for:

transition-property: color;
transition-duration: 1s;
transition-timing-function: linear;

Images

Responsive Images work well on devices with different screen sizes and resolutions. Add:

img {
  width: 100%;
}

Use width: 100% on images and videos to set the width to whatever the image's container's width is. CSS also provides control over aspect ratio which is especially useful for videos.

The problem with our responsive image as currently coded can be observed by throttling the download speed, turning off Cache in the Network tab of the dev tools and doing a hard refresh by right clicking on the browser's refresh icon.

Note the Cumulative Layout Shift which occurs.

(Be sure to turn off throttling and enable cache before continuing.)

Replace the lone img tag in the HTML with <figure> and <figcaption> tags:

<figure>
  <img
    src="img/pesto.jpg"
    itemprop="image"
    alt="a bold of pesto with a wooden spoon in it"
  />
  <figcaption>
    Classic, simple basil pesto recipe with fresh basil leaves, pine nuts,
    garlic, Romano or Parmesan cheese, extra virgin olive oil, and salt and
    pepper.
  </figcaption>
</figure>

The <figure> tag allows an optional <figcaption> element.

Aside - Image Optimization

We want to display identical image content across devices but the dimensions should be larger or smaller depending on the device - e.g. a smaller image for mobile users. While the standard <img> element points the browser to a single source file, two attributes — srcset and sizes — provide additional source images along with hints to help the browser pick the right one.

srcset defines a set of images that allow the browser to choose which image size to use.

Examine image use in a typical NY Times article:

<img
  alt="A man in a purple hoodie talks to a group of teenagers. "
  class="css-jb88yk"
  src="https://static01.nyt.com/images/2023/06/01/multimedia/00ny-no-cops90-vkgl/00ny-no-cops90-vkgl-articleLarge.jpg?quality=75&amp;auto=webp&amp;disable=upscale"
  srcset="
    https://static01.nyt.com/images/2023/06/01/multimedia/00ny-no-cops90-vkgl/00ny-no-cops90-vkgl-articleLarge.jpg?quality=75&amp;auto=webp  600w,
    https://static01.nyt.com/images/2023/06/01/multimedia/00ny-no-cops90-vkgl/00ny-no-cops90-vkgl-jumbo.jpg?quality=75&amp;auto=webp        1024w,
    https://static01.nyt.com/images/2023/06/01/multimedia/00ny-no-cops90-vkgl/00ny-no-cops90-vkgl-superJumbo.jpg?quality=75&amp;auto=webp   2048w
  "
  sizes="100vw"
  decoding="async"
  width="600"
  height="400"
/>

Note the width and height attributes to prevent layout shift.

Replace the img tag in index.html with:

<img
  alt="a bowl of pesto with a wooden spoon in it"
  src="img/pesto.jpg"
  srcset="
    img/pesto/pesto_iodywc_c_scale,w_380.jpg   600w,
    img/pesto/pesto_iodywc_c_scale,w_780.jpg  1024w,
    img/pesto/pesto_iodywc_c_scale,w_1380.jpg 2048w
  "
  width="100%"
  height="auto"
/>

Hard reload the page at various widths and look in the Sources panel of the developer tools to see the image that was chosen by the browser.


Note: you might also see a picture tag used on occasion:

<picture>
  <source
    srcset="img/pesto/pesto_iodywc_c_scale,w_1380.jpg"
    media="(min-width: 1200px)"
  />
  <source
    srcset="img/pesto/pesto_iodywc_c_scale,w_780.jpg"
    media="(min-width: 800px)"
  />
  <img itemprop="image" src="img/pesto.jpg" />
</picture>

The <picture> tag is used for cropping or modifying images for different media conditions or offering different image formats when certain formats are not supported by all browsers. See the example on MDN.

The browser ignores everything after the first matching condition, so be careful how you order the media conditions.

Using either technique can save a lot of bandwidth. Older browsers that don't support these features will just ignore them and load the image referenced in the src attribute.

You typically automate the process using software such as Sharp to create multiple image sizes and formats. The HTML is often automatically generated as well.

To experiment with these techniques I uploaded pesto.jpg to responsivebreakpoints.com, downloaded the zip file and place the unzipped folder in the img directory.

There are also specialized image only hosting services such as Cloudinary which perform image processing.

Demo: samples of Cloudinary image processing:

<img
  src="https://res.cloudinary.com/deedee/image/upload/w_200,h_200,c_crop/v1623521871/samples/pesto.jpg"
  alt="basil"
/>

<img
  src="https://res.cloudinary.com/deedee/image/upload/w_200,e_grayscale/v1623521871/samples/pesto.jpg"
  alt="basil"
/>

The techniques above are used primarily on high traffic websites. For smaller sites just run your images through a processor such as imageOptim before deploying them on your site.

Flex Layout

The two column view applies only to widescreen.

We will make the article and aside run side by side by applying flex to their parent container within a mobile first breakpoint:

@media (width > 640px) {
  .content {
    display: flex;
  }
}

Note: we cannot use a CSS variable as a breakpoint, i.e.:

@media (min-width: var(--breakpoint)) {
  /* ... */
}

A media query is not an element selector so it does not inherit.

We can use the flex property on the flex children to manipulate the columns:

@media (width > 640px) {
  .content {
    display: flex;
  }
  article {
    flex: 1 0 60%;
  }
}

The flex property is used on flex children only. We are using a shortcut here which includes flex-grow, flex-shrink, and flex-basis. The default is 0 1 auto.

Here is the long form:

article {
  flex-grow: 1;
  flex-shrink: 0;
  flex-basis: 60%;
}

Use a background color and box-shadow to color the aside:

@media (width > 640px) {
  /* ommited for brevity */
  aside {
    background: var(--light-green);
    box-shadow: -4px 0px 4px #ddd;
  }
}

Add a variable for light green:

--light-green: #fafdeb;

Add some padding to the two columns for both large and small screens:

article,
aside {
  padding: 1rem;
}

Format the footer;

footer {
  background-color: var(--basil-green);
  padding: 1rem;
  border-radius: 0 0 4px 4px;
  margin-bottom: 2rem;
}

The Branding Header

Add the green background to the branding div.

header {
  height: 120px;
  background: var(--basil-green);
  border-radius: 8px 8px 0px 0px;
}

Note: this is one of the rare occasions that we use the height property. We can use it here because the header does not contain dynamic content.

Custom Fonts

Add a custom font (top of the css file). Copy the @font-face CSS from font/stylesheet.css into the top of styles.css and correct the file paths:

@font-face {
  font-family: "futura_stdlight";
  src:
    url("font/futurastd-light-webfont-webfont.woff2") format("woff2"),
    url("font/futurastd-light-webfont-webfont.woff") format("woff");
  font-weight: normal;
  font-style: normal;
}

Note - To convert fonts to web formats see Font Squirrel.

header h1 {
  background: url(img/basil.png) no-repeat;
  font-family: futura_stdlight, sans-serif;
  font-weight: normal;
  color: #fff;
  font-size: 5rem;
}

Note: font-weight: normal; is nice to have here because by default header tags are bold and we do not have a bold version of the futura font available.

Try: temporarily remove no-repeat from the background property and setting the height of the <h1> to 600px.

The background image is 272px by 170px. Recall the final design goal.

Since background images fill the background of their container we can begin by manipulating padding:

header h1 {
  /* omitted */
  padding-left: 260px;
  padding-top: 90px;
}

(We cannot see the text because it is white and we have added padding.)

Use transform to tweak the positioning:

header h1 {
  /* omitted */
  transform: translate(-100px, -80px);
}

Note: transforms are a useful property, especially when it comes to creating animations.

CSS Nesting

In order to simplify your CSS and maintain order you can nest one CSS rule inside another:

header {
  height: 120px;
  background: var(--basil-green);
  border-radius: 8px 8px 0px 0px;
  h1 {
    background: url(img/basil.png) no-repeat;
    font-family: futura_stdlight, sans-serif;
    font-weight: normal;
    color: #fff;
    font-size: 5rem;
    padding-left: 260px;
    padding-top: 90px;
    transform: translate(-100px, -80px);
  }
}

Note the beta link in the header:

<header>
  <h1>Basilica!</h1>
  <a class="beta" href="#">Beta</a>
</header>

Absolutely position the beta element:

header a.beta {
  background: url("img/burst.svg") no-repeat;
  color: #fff;
  font-size: 1.5rem;
  position: absolute;
  top: -20px;
  right: 10px;
  width: 85px;
  height: 85px;
  line-height: 85px;
  text-align: center;
  text-transform: uppercase;
}

We can use flex in order to align the text vertically and horizontally:

header a.beta {
  background: url("img/burst.svg") no-repeat;
  color: #fff;
  font-size: 1.5rem;
  position: absolute;
  top: -20px;
  right: 10px;
  width: 85px;
  height: 85px;
  /* line-height: 85px; */
  /* text-align: center; */
  text-transform: uppercase;
  display: flex;
  justify-content: center;
  align-items: center;
}

Note:

  • the use of img/burst.svg for the background image. Examine it in the editor.
  • the use of position absolute. We will give this element a positioning context by applying position absolute to its containing element:
header {
  /* omitted */
  position: relative;
}

Add a hover, transform and animate:

header a.beta {
  /* omitted */
  transform: rotate(20deg);
  transition: all 1s ease;
}

Create a hover state for the burst:

header a.beta:hover {
  transform: rotate(0deg) scale(1.2);
}

Finally, nest the beta element inside the header CSS:

header {
  height: 120px;
  background: var(--basil-green);
  border-radius: 8px 8px 0px 0px;
  position: relative;
  h1 {
    background: url(img/basil.png) no-repeat;
    font-family: futura_stdlight, sans-serif;
    font-weight: normal;
    color: #fff;
    font-size: 5rem;
    padding-left: 260px;
    padding-top: 90px;
    transform: translate(-100px, -80px);
  }
  a.beta {
    background: url("img/burst.svg") no-repeat;
    color: #fff;
    font-size: 1.5rem;
    position: absolute;
    top: -20px;
    right: 10px;
    width: 85px;
    height: 85px;
    text-transform: uppercase;
    display: flex;
    justify-content: center;
    align-items: center;
    transform: rotate(20deg);
    transition: all 1s ease;
    &:hover {
      transform: rotate(0deg) scale(1.2);
    }
  }
}

Note the use of the ampersand (&) for the nested hover.

Header: Responsive Design

Examine the page for problems in a narrow browser.

We will attempt a mobile first design strategy. Edit the css to display for small screen first:

header {
  /* omitted */
  h1 {
    background: url(img/basil.png) no-repeat;
    font-family: futura_stdlight, sans-serif;
    font-weight: normal;
    color: #fff;
    font-size: 5rem;
  }

And add features for the large screen within the media query:

header {
  /* omitted */
  h1 {
    background: url(img/basil.png) no-repeat;
    font-family: futura_stdlight, sans-serif;
    font-weight: normal;
    color: #fff;
    font-size: 5rem;
    @media (width > 640px) {
      padding-left: 240px;
      padding-top: 90px;
      transform: translate(-100px, -80px);
      background-position: top left;
    }
  }
}

Additional tweaks for the small screen:

  • Remove the body margin top:
body {
  font-family: "Lucida Sans", "Lucida Sans Regular", "Lucida Grande",
    "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
  line-height: 1.5;
  color: var(--dark-gray);
  max-width: var(--max-width);
  /* margin: 0 auto;
  margin-top: 24px; */
}

and add it back for the wide screen:

@media (min-width: 640px) {
  margin: 0 auto;
  margin-top: 24px;
}
  • Remove the rounded corners on small screen:
header {
  /* omitted */
  /* border-radius: 8px 8px 0px 0px; */
}

add them back to wide screens:

header {
  height: 120px;
  background: var(--basil-green);
  /* border-radius: 8px 8px 0px 0px; */
  position: relative;
  @media (width > 640px) {
    border-radius: 8px 8px 0px 0px;
  }
  /* omitted */

Always remember: there is no hover in touch screen devices. Use the Device Toggle in the developer tools set to a mobile viewport and tap on the burst.

The Navigation

Add the code below:

nav {
  background: var(--light-gray);
  border-top: 0.5rem solid var(--light-orange);
  padding: 0.5rem;
  display: flex;
  align-items: center;

  ul {
    display: flex;
    gap: 1rem;
  }

  li {
    list-style: none;
    /* margin-right: 0.5rem; */
  }
  p {
    margin-right: auto;
  }
}

Note:

  • the light gray needs to be lighter: --light-gray: #e4e1d1;
  • we have two flex children, the <ul> and the lone <p> tag.
  • the margin-right property on the paragraph and the effect it has on the positioning on the navigation links.

Remove it and add justify-content to the flex parent:

nav {
  justify-content: space-between;
  flex-wrap: wrap;

  /* p {
  margin-right: auto; 
} */
}

Note: the flex-wrap property allows the paragraph to stack on small screens.

Nav Links and Gradients

nav {
  /* omitted */
  a {
    text-align: center;
    font-size: 1.5rem;
    padding: 8px;
    color: #fff;
    text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5);
    border-radius: 6px;
  }
}

The gradients for the buttons:

.nav-storeit a {
  background-image: linear-gradient(to bottom, #fcde41 1%, #dfa910 100%);
}

.nav-storeit a:hover {
  background-image: linear-gradient(to bottom, #dfa910 0%, #fcde41 100%);
}

.nav-pickit a {
  background-image: linear-gradient(to bottom, #abc841 0%, #6b861e 100%);
}

.nav-pickit a:hover {
  background-image: linear-gradient(to bottom, #6b861e 1%, #abc841 100%);
}

.nav-cookit a {
  background-image: linear-gradient(to bottom, #6f89c7 0%, #344e8b 100%);
}

.nav-cookit a:hover {
  background-image: linear-gradient(to bottom, #344e8b 1%, #6f89c7 100%);
}

The red color we've chosen for hovers is not visually pleasant here. We will use the CSS pseudo-class :not to exclude the links in the nav bar:

nav {
  /* omitted */
  a {
    /* omitted */
    &:hover {
      color: white;
    }
  }
}

Make all the buttons the same width. Try with and without the inline-block.

nav {
  /* omitted */
  a {
    /* omitted */
    min-width: 120px;
    display: inline-block;
  }
}

Note: this is a setting which will likely need to be changed to accomodate small screens.

CSS Grid

CSS Tricks offers a guide to CSS grid.

Flexbox operates in a single dimension using the flex-direction property: x or y. CSS Grid operates on both the x and y axis.

Our current use of Flexbox to style the content columns operates in a single (horizontal or x) dimension so flex is really all we need for this layout.

Nevertheless, we will use CSS Grid for the layout in order to introduce some of its features in a simple use case.

Remove the flex statements and use a grid display, define columns, and set the start and end points for the grid children:

@media (width > 640px) {
  .content {
    /* display: flex; */
    display: grid;
    grid-template-columns: 20% 20% 20% 20% 20%;
  }
  article {
    grid-column-start: 1;
    grid-column-end: span 3;
    /* flex: 1 0 60%; */
  }
  aside {
    grid-column-start: 4;
    grid-column-end: span 2;
    background: var(--light-green);
    box-shadow: -4px 0px 4px #ddd;
  }
}

grid-template-columns can also be expressed as a series of fractions:

grid-template-columns: 1fr 1fr 1fr 1fr 1fr;

Or using shorthand:

grid-template-columns: repeat(5, 1fr);

By moving the display: grid setting to the body selector, we can use grid areas to define our layout:

@media (width > 600px) {
  body {
    display: grid;
    grid-template-areas:
      "header"
      "nav"
      "content"
      "footer";
  }
  header {
    grid-area: header;
  }
  nav {
    grid-area: nav;
  }
  .content {
    grid-area: content;
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    grid-column-gap: 1rem;
  }
  article {
    grid-column: span 3;
  }
  aside {
    grid-column: span 2;
    background: var(--light-green);
    box-shadow: -4px 0px 4px #ddd;
  }
  footer {
    grid-area: footer;
  }
}

Demo: with grid areas it becomes easy to reassign different sections of the page.

header {
  grid-area: footer;
}

This can be very useful in conjunction with breakpoints for responsively laying out complex pages.

JavaScript

Let's ease back into JavaScript with a demonstration and a simple DOM manipulation.

Aside: Demo Arrays in Node

Review Node:

$ mkdir node
$ cd node
$ touch basilnode.js
$ npm init -y
$ npm install random-number

Random Number on npmjs.com.

In basilnode.js:

const randomNumber = require("random-number");

const randomIndex = randomNumber({
  min: 0,
  max: 4,
  integer: true,
});

console.log(randomIndex);
console.log(typeof randomNumber);
console.log(typeof randomIndex);

At the command line:

$ node basilnode.js

Add some additional variables - arrays:

const randomNumber = require("random-number");

const basilChef = ["mama", "papa", "baby"];

function randomItem(array) {
  const randomIndex = randomNumber({
    min: 0,
    max: array.length - 1,
    integer: true,
  });
  return array[randomIndex];
}

console.log(basilChef);
console.log(basilChef[0]);
console.log(basilChef.length);
console.log(randomItem(basilChef));
$ node basilnode.js

Call the randomItem function from within another function and use a template string to construct a bit of HTML::

const randomNumber = require("random-number");

const basilChef = ["mama", "papa", "baby"];
const basilTexture = ["greasy", "frozen", "spicy"];

function randomItem(array) {
  const randomIndex = randomNumber({
    min: 0,
    max: array.length - 1,
    integer: true,
  });
  return array[randomIndex];
}

function makeBasil() {
  return `<h2>${randomItem(basilChef)}'s ${randomItem(
    basilTexture
  )} basil</h2>`;
}

console.log(makeBasil());

Here is the same random number generation above without relying on the installed node module:

function random() {
  const max = 3;
  const randomIndex = Math.floor(Math.random() * max);
  return randomIndex;
}

const basilChef = ["mama", "papa", "baby"];
const basilTexture = ["greasy", "frozen", "spicy"];

function randomItem(array) {
  const randomIndex = random();
  return array[randomIndex];
}

function makeBasil() {
  return `<h2>${randomItem(basilChef)}'s ${randomItem(
    basilTexture
  )} basil</h2>`;
}

console.log(makeBasil());

Add a Script

<script src="js/index.js"></script>

We'll do something similar to the node demo above in our app - replacing the recipe title with a random one.

In index.js:

Evolve a function that uses JavaScript's built-in Math methods to return a random number between zero and two:

function random() {
  const max = 3;
  // const randomIndex = Math.random();
  // const randomIndex = Math.random() * max;
  const randomIndex = Math.floor(Math.random() * max);
  return randomIndex;
}

console.log(random());

Now call our random function passing in an array.

const basilChefs = ["mama", "papa", "baby"];

function random(array) {
  const max = array.length;
  const randomIndex = Math.floor(Math.random() * max);
  return array[randomIndex];
}

const recipeName = random(basilChefs);
console.log(recipeName);

We used the random number to select a name from the array and return it to the calling function.

Add another variable basilTexture and massage the output to product a string:

const basilChefs = ["mama", "papa", "baby"];
const basilTexture = ["greasy", "frozen", "spicy"];

function random(array) {
  const max = array.length;
  const randomIndex = Math.floor(Math.random() * max);
  return array[randomIndex];
}

const recipeName =
  "My " + random(basilChefs) + "'s " + random(basilTexture) + " pesto";
console.log(recipeName);

Let's use the return value in our layout:

const titleElement = document.querySelector("h2");

Test titleElement in the console.

const titleElement = document.querySelector("h2");

const basilChefs = ["mama", "papa", "baby"];
const basilTexture = ["greasy", "frozen", "spicy"];

function random(array) {
  const max = array.length;
  const randomIndex = Math.floor(Math.random() * max);
  return array[randomIndex];
}

const recipeName = `${random(basilChefs)}'s ${random(basilTexture)} pesto`;

titleElement.innerText = recipeName;

and add CSS:

article h2 {
  font-size: 2rem;
  text-transform: capitalize;
}

JavaScript Popover

Create a div on the bottom of the html page (but before the script tag).

<div class="modal">
  <h3>Hi! I'm a Modal Window (ʘ‿ʘ)╯</h3>
  <p>Information about the beta program.</p>
</div>

Add styling:

.modal {
  box-sizing: border-box;
  max-width: 400px;
  padding: 2rem;
  border-radius: 5px;
  min-height: 200px;
  border: 2px solid var(--orange);
  background: white;
  /* position: fixed;
  top: calc(50vh - 100px);
  left: calc(50vw - 200px); */
  /* display: none; */
  place-self: center;
}

Uncomment display: none and add a open rule:

.open {
  display: block;
}

Test by adding the open class to the modal using the dev tool's inspector.

Code the .beta button to show the window.

Create a variable for the beta button, attach an event listener to it, and create a function to handle the event.

const modal = document.querySelector(".modal");
const betaButton = document.querySelector(".beta");

function showPopover(event) {
  modal.classList.toggle("open");
  event.preventDefault();
}

betaButton.addEventListener("click", showPopover);

Refactor to use event delegation:

const modal = document.querySelector(".modal");
// const betaButton = document.querySelector('.beta');

function showPopover(event) {
  console.log(event.target);
  if (!event.target.matches(".beta")) return;
  modal.classList.toggle("open");
  event.preventDefault();
}

// betaButton.addEventListener('click', showPopover);
document.addEventListener("click", showPopover);

DOM Scripting Methods Used

Matches

The matches() method lets you check if an element would be selected by a particular selector. It returns true if the element is a match, and false when it’s not. It can be an alternative to using element.classList.contains('.someclass').

// Match by an ID
if (elem.matches("#first-button")) {
  // Do something...
}

// Match by a class
if (elem.matches(".button-submit")) {
  // Do something...
}

// Match by one of several selectors
// Returns true when element contains at least one of the selectors
if (elem.matches(".click-me, .button-submit")) {
  // Do something...
}

Add Another Close Method

Add html to the betainfo:

<div class="modal">
  <h3>Hi! I'm a Modal Window (ʘ‿ʘ)╯</h3>
  <p>Information about the beta program.</p>
  <!-- NEW -->
  <a class="closer" href="#0">✖︎</a>
</div>

Style it:

.closer {
  position: absolute;
  top: -10px;
  right: -10px;
  width: 1.5rem;
  height: 1.5rem;
  background: #fff;
  color: var(--orange);
  border: 4px solid var(--orange);
  border-radius: 50%;
  text-align: center;
  line-height: 1.5rem;
  cursor: pointer;
}

Extend the showPopover function to include the new element script.

const modal = document.querySelector(".modal");

function showPopover(event) {
  if (!event.target.matches(".beta, .closer")) return;
  modal.classList.toggle("open");
  event.preventDefault();
}

document.addEventListener("click", showPopover);

Note: you cannot animate between display: none and display: block.

Adding Animation to the Modal

Add a wrapping div - modal-outer - around the modal:

<div class="modal-outer">
  <div class="modal">
    <h3>Hi! I'm a Modal Window (ʘ‿ʘ)╯</h3>
    <p>Information about the beta program.</p>
    <a class="closer" href="#0">✖︎</a>
  </div>
</div>

Style it:

.modal-outer {
  display: grid;
  justify-content: center;
  align-items: center;
  background: rgba(0, 0, 0, 0.5);
  position: fixed;
  height: 100vh;
  width: 100vw;
  top: 0;
  left: 0;
  /* Hide this until we need it */
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s;
}

.modal-outer.open {
  opacity: 1;
  pointer-events: all;
}

Try: changing the opacity and pointer-events properties to 1 and all.

Edit the script to select the outer div and apply the open class to it:

const modal = document.querySelector(".modal");
const modalOuter = document.querySelector(".modal-outer");

function showPopover(event) {
  if (!event.target.matches(".beta, .closer")) return;
  modalOuter.classList.toggle("open");
  event.preventDefault();
}

document.addEventListener("click", showPopover);

Now the modal wrapper will show when the button is clicked - but the modal will not. (Toggle the opacity on the outer div to see the modal.)

Edit styles for the interior modal:

.modal {
  box-sizing: border-box;
  max-width: 400px;
  padding: 2rem;
  border-radius: 5px;
  min-height: 200px;
  border: 2px solid var(--orange);
  background: white;
  transform: translateY(200%);
  transition: transform 1s;
}

Note: we are removing the following properties from the inner modal:

position: fixed;
top: 30%;
left: calc(50% - 150px);
display: none;

Remove the generic open CSS rule:

/* .open {
  display: block;
} */

and replace it with a transform that selects the modal via the outer modal's class:

.modal-outer.open .modal {
  transform: translateY(0);
}

Note that we are no longer using 'display: none to hide the modal. The inner modal is becoming visible because its container, modal outer, is transitioning opacity.

Edit the script to allow clicking on the overlay to close the modal.

// const modal = document.querySelector('.modal')
const modalOuter = document.querySelector(".modal-outer");

function showPopover(event) {
  if (event.target.matches(".beta")) {
    modalOuter.classList.add("open");
  } else if (event.target.matches(".closer, .modal-outer")) {
    modalOuter.classList.remove("open");
  } else return;
  event.preventDefault();
}

document.addEventListener("click", showPopover);

A Dynamic Popover

We will use the popover for different purposes depending on which element is clicked.

const modalOuter = document.querySelector(".modal-outer");
const modalInner = document.querySelector(".modal");

var betaContent = `
<h3>Oooops!</h3>
<p>Wow! Nothing works!<p>
`;

function showPopover(event) {
  if (event.target.matches(".beta")) {
    modalInner.innerHTML = betaContent;
    modalOuter.classList.add("open");
  } else if (event.target.matches(".closer, .modal-outer")) {
    modalOuter.classList.remove("open");
  } else return;
  event.preventDefault();
}

document.addEventListener("click", showPopover);

Let's use our new popover to display a different message when the user clicks on any of the three nav buttons.

const modalOuter = document.querySelector(".modal-outer");
const modalInner = document.querySelector(".modal");

var betaContent = `
<h3>Oooops!</h3>
<p>Wow! Nothing works!<p>
`;
var buttonContent = `
<h2>Coming Soon</h2>
<p>This feature coming soon.<p>
<a class="closer" href="#0">✖︎</a>
`;

function showPopover(event) {
  if (event.target.matches(".beta")) {
    modalInner.innerHTML = betaContent;
    modalOuter.classList.add("open");
  } else if (event.target.closest("nav ul")) {
    modalInner.innerHTML = buttonContent;
    modalOuter.classList.add("open");
  } else if (event.target.matches(".closer, .modal-outer")) {
    modalOuter.classList.remove("open");
  } else return;
  event.preventDefault();
}

document.addEventListener("click", showPopover);

Note the use of closest above. The closest() method looks for the closest matching parent to an element that has a selector that you pass in.

END

HTML Dialog Element

<dialog class="modal">
  <h3>Hi! I'm a Modal Window (ʘ‿ʘ)╯</h3>
  <p>Information about the beta program.</p>
  <form method="dialog">
    <button value="cancel">Cancel</button>
    <button value="confirm">Confirm</button>
  </form>
</dialog>
// Get the button and modal let btn =
document.querySelector("[data-show-modal]");
let modal = document.querySelector("dialog"); // Show the modal
btn.addEventListener("click", function () {
  modal.showModal();
});
modal.addEventListener("close", function (event) {
  if (modal.returnValue !== "confirm") return;
  console.log("Confirmed!");
  // Do something...
});
dialog::backdrop {
  background-color: rgba(0, 0, 0, 0.7);
}

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors