diff --git a/README.md b/README.md index db5fe2b..7da49b4 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ - Buttons - Forms - Editable String +- Navigation & Overlays - Lists - Badges - Alerts diff --git a/demo/index.html b/demo/index.html index f7117ec..2e15e0d 100644 --- a/demo/index.html +++ b/demo/index.html @@ -48,6 +48,11 @@
  • + + Navigation & Overlays + +
  • +
  • Lists @@ -92,6 +97,7 @@ @@include("partials/buttons.html") @@include("partials/forms.html") @@include("partials/editable-string.html") + @@include("partials/navigation-overlays.html") @@include("partials/lists.html") @@include("partials/badges.html") @@include("partials/alerts.html") diff --git a/demo/partials/navigation-overlays.html b/demo/partials/navigation-overlays.html new file mode 100644 index 0000000..70ca63d --- /dev/null +++ b/demo/partials/navigation-overlays.html @@ -0,0 +1,135 @@ + diff --git a/src/js/components/overlays.js b/src/js/components/overlays.js new file mode 100644 index 0000000..9f3db8d --- /dev/null +++ b/src/js/components/overlays.js @@ -0,0 +1,140 @@ +let initialized = false; + +function closeNode(node) { + node.classList.remove("is-open"); + node.querySelectorAll("[aria-expanded='true']").forEach(trigger => { + trigger.setAttribute("aria-expanded", "false"); + }); +} + +function closeAll(except = null) { + document.querySelectorAll(".dropdown.is-open, .popover.is-open, .tooltip.is-open").forEach(node => { + if(node !== except) { + closeNode(node); + } + }); +} + +function initTabs(root = document) { + root.addEventListener("click", event => { + const tab = event.target.closest(".tab"); + + if(!tab) { + return; + } + + const tabs = tab.closest(".tabs"); + + if(!tabs) { + return; + } + + tabs.querySelectorAll(".tab").forEach(item => { + const isActive = item === tab; + item.classList.toggle("tab-active", isActive); + item.setAttribute("aria-selected", String(isActive)); + }); + }); +} + +function initDropdowns(root = document) { + root.addEventListener("click", event => { + const trigger = event.target.closest("[data-dropdown-toggle]"); + + if(!trigger) { + return; + } + + const dropdown = trigger.closest(".dropdown"); + + if(!dropdown) { + return; + } + + event.preventDefault(); + const isOpen = dropdown.classList.contains("is-open"); + closeAll(dropdown); + dropdown.classList.toggle("is-open", !isOpen); + trigger.setAttribute("aria-expanded", String(!isOpen)); + }); +} + +function initPopovers(root = document) { + root.addEventListener("click", event => { + const trigger = event.target.closest("[data-popover-toggle]"); + + if(!trigger) { + return; + } + + const popover = trigger.closest(".popover"); + + if(!popover) { + return; + } + + event.preventDefault(); + const isOpen = popover.classList.contains("is-open"); + closeAll(popover); + popover.classList.toggle("is-open", !isOpen); + trigger.setAttribute("aria-expanded", String(!isOpen)); + }); +} + +function initTooltips(root = document) { + root.addEventListener("click", event => { + const trigger = event.target.closest("[data-tooltip-toggle]"); + + if(!trigger) { + return; + } + + const tooltip = trigger.closest(".tooltip"); + + if(!tooltip) { + return; + } + + event.preventDefault(); + const isOpen = tooltip.classList.contains("is-open"); + closeAll(tooltip); + tooltip.classList.toggle("is-open", !isOpen); + }); +} + +function initDismiss(root = document) { + root.addEventListener("click", event => { + if(event.target.closest(".dropdown, .popover, .tooltip")) { + return; + } + + closeAll(); + }); + + document.addEventListener("keydown", event => { + if(event.key === "Escape") { + closeAll(); + } + }); +} + +function init(root = document) { + if(root === document && initialized) { + return; + } + + initTabs(root); + initDropdowns(root); + initPopovers(root); + initTooltips(root); + initDismiss(root); + + if(root === document) { + initialized = true; + } +} + +export default { + init, + closeAll +}; diff --git a/src/js/index.js b/src/js/index.js index 7ab1903..b4ac6a2 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -4,6 +4,7 @@ import advancedSelect from "./components/advanced-select.js"; import editableString from "./components/editable-string.js"; import confirmPopup from "./components/confirm-popup.js"; +import Overlays from "./components/overlays.js"; import demoNavigation from "./demo-navigation.js"; import codeExamples from "./code-examples.js"; @@ -13,12 +14,14 @@ Modals, advancedSelect, editableString, - confirmPopup + confirmPopup, + Overlays }; Object.assign(window, api); document.addEventListener("DOMContentLoaded", () => { + Overlays.init(); demoNavigation(); codeExamples(); }); @@ -29,7 +32,8 @@ Modals, advancedSelect, editableString, - confirmPopup + confirmPopup, + Overlays }; export default api; diff --git a/src/scss/components/_badges.scss b/src/scss/components/_badges.scss index d886ffc..4eb598e 100644 --- a/src/scss/components/_badges.scss +++ b/src/scss/components/_badges.scss @@ -1,6 +1,6 @@ .badge { background: $color-primary; - color: $color-dark; + color: $color-black; padding: $space-1 $space-2; font-size: $font-size-sm; font-weight: $font-weight-semibold; @@ -26,12 +26,12 @@ &.badge-info { background: $color-info; - color: $color-text-light; + color: $color-black; } &.badge-secondary { background: $color-secondary; - color: $color-text-light; + color: $color-black; } &.badge-primary-outline { diff --git a/src/scss/components/_buttons.scss b/src/scss/components/_buttons.scss index dac7005..6219a82 100644 --- a/src/scss/components/_buttons.scss +++ b/src/scss/components/_buttons.scss @@ -37,7 +37,7 @@ .ph, .ph-bold { position: absolute; - color: #0a0a0d; + color: $color-black; left: -$control-icon-rail-md; top: 1px; font-size: $icon-size-lg; @@ -76,12 +76,12 @@ @include hover_touch { background-color: $color-secondary; - color: $color-text-light; + color: $color-black; &.with-icon { .ph, .ph-bold { - color: $color-text-light; + color: $color-black; } } } @@ -93,12 +93,12 @@ @include hover_touch { background-color: $color-accent; - color: $color-text-light; + color: $color-black; &.with-icon { .ph, .ph-bold { - color: $color-text-light; + color: $color-black; } } } @@ -140,12 +140,12 @@ @include hover_touch { background-color: $color-info; - color: $color-text-light; + color: $color-black; &.with-icon { .ph, .ph-bold { - color: $color-text-light; + color: $color-black; } } } diff --git a/src/scss/components/_cards.scss b/src/scss/components/_cards.scss index dcdaf5e..801f6de 100644 --- a/src/scss/components/_cards.scss +++ b/src/scss/components/_cards.scss @@ -105,7 +105,7 @@ } &.card-info { - @include state_panel($color-info, $color-text-light); + @include state_panel($color-info); .status-icon { color: $color-info; @@ -113,7 +113,7 @@ } &.card-secondary { - @include state_panel($color-secondary, $color-text-light); + @include state_panel($color-secondary); .status-icon { color: $color-secondary; diff --git a/src/scss/components/_navigation-overlays.scss b/src/scss/components/_navigation-overlays.scss new file mode 100644 index 0000000..e6739a3 --- /dev/null +++ b/src/scss/components/_navigation-overlays.scss @@ -0,0 +1,170 @@ +.tabs { + display: flex; + flex-wrap: wrap; + gap: $space-2; + align-items: center; + margin-bottom: $space-4; + + .tab { + display: inline-flex; + align-items: center; + min-height: $control-height-sm; + padding: $space-2 $space-3; + border: $border-width-base solid $border-color-muted; + border-left-width: $border-width-accent; + color: $color-text-medium; + background: $surface-panel-muted; + font-family: $font-family-base; + font-size: $font-size-sm; + font-weight: $font-weight-semibold; + line-height: $line-height-base; + text-transform: uppercase; + cursor: pointer; + transition-duration: $motion-base; + transition-timing-function: $motion-ease; + transition-property: color, background, border-color; + + &:focus-visible { + @include focus_ring; + } + + @include hover_touch { + color: $color-black; + background: $color-secondary; + border-color: $color-secondary; + } + + &.tab-active, + &[aria-selected="true"] { + color: $color-black; + background: $color-primary; + border-color: $color-primary; + } + } +} + +.dropdown, +.popover { + position: relative; + display: inline-flex; +} + +.dropdown-menu, +.popover-panel, +.tooltip-panel { + z-index: 40; + background: $surface-panel; + border: $border-width-base solid $border-color-muted; + border-left-width: $border-width-accent; + box-shadow: 0 14px 36px rgba($color-black, 0.36); +} + +.dropdown-menu, +.popover-panel { + position: absolute; + top: calc(100% + #{$space-2}); + left: 0; + min-width: 220px; + display: none; +} + +.dropdown.is-open .dropdown-menu, +.popover.is-open .popover-panel { + display: block; +} + +.dropdown-menu { + padding: $space-1; + + .dropdown-item { + display: flex; + align-items: center; + gap: $space-2; + width: 100%; + min-height: $control-height-sm; + padding: $space-2 $space-3; + border: 0; + color: $color-text-light; + background: transparent; + font-family: $font-family-base; + font-size: $font-size-sm; + text-align: left; + cursor: pointer; + transition-duration: $motion-base; + transition-timing-function: $motion-ease; + transition-property: color, background; + + .ph, + .ph-bold { + font-size: $icon-size-sm; + } + + &:focus-visible { + @include focus_ring; + } + + @include hover_touch { + color: $color-black; + background: $color-secondary; + } + + &.dropdown-item-danger { + color: $color-error; + + @include hover_touch { + color: $color-black; + background: $color-error; + } + } + } +} + +.popover-panel { + width: min(320px, calc(100vw - #{$space-6})); + padding: $space-4; + + .popover-title { + margin: 0 0 $space-2; + font-size: $font-size-md; + font-weight: $font-weight-bold; + text-transform: uppercase; + } + + .popover-text { + margin: 0; + color: $color-text-medium; + font-size: $font-size-sm; + line-height: $line-height-relaxed; + } +} + +.tooltip { + position: relative; + display: inline-flex; +} + +.tooltip-panel { + position: absolute; + left: 50%; + bottom: calc(100% + #{$space-2}); + width: max-content; + max-width: 260px; + padding: $space-2 $space-3; + color: $color-text-light; + font-size: $font-size-xs; + line-height: $line-height-normal; + transform: translateX(-50%); + opacity: 0; + visibility: hidden; + pointer-events: none; + transition-duration: $motion-fast; + transition-timing-function: $motion-ease; + transition-property: opacity, visibility; +} + +.tooltip:hover .tooltip-panel, +.tooltip:focus-within .tooltip-panel, +.tooltip.is-open .tooltip-panel { + opacity: 1; + visibility: visible; +} diff --git a/src/scss/components/_toasts.scss b/src/scss/components/_toasts.scss index 7cc7642..82e9987 100644 --- a/src/scss/components/_toasts.scss +++ b/src/scss/components/_toasts.scss @@ -59,7 +59,7 @@ width: $control-height-md; &:hover { - color: $color-dark; + color: $color-black; } } @@ -68,7 +68,6 @@ .toast-title { background: $color-info; - color: $color-text-light; } } diff --git a/src/scss/kit.scss b/src/scss/kit.scss index 5db9871..c0eef79 100644 --- a/src/scss/kit.scss +++ b/src/scss/kit.scss @@ -17,6 +17,7 @@ @import "components/alerts"; @import "components/advanced-select"; @import "components/editable-string"; +@import "components/navigation-overlays"; @import "utils"; * {