summaryrefslogtreecommitdiffstats
path: root/toolkit/content/widgets/moz-page-nav
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:13:27 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:13:27 +0000
commit40a355a42d4a9444dc753c04c6608dade2f06a23 (patch)
tree871fc667d2de662f171103ce5ec067014ef85e61 /toolkit/content/widgets/moz-page-nav
parentAdding upstream version 124.0.1. (diff)
downloadfirefox-40a355a42d4a9444dc753c04c6608dade2f06a23.tar.xz
firefox-40a355a42d4a9444dc753c04c6608dade2f06a23.zip
Adding upstream version 125.0.1.upstream/125.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/content/widgets/moz-page-nav')
-rw-r--r--toolkit/content/widgets/moz-page-nav/moz-page-nav-button.css123
-rw-r--r--toolkit/content/widgets/moz-page-nav/moz-page-nav.css76
-rw-r--r--toolkit/content/widgets/moz-page-nav/moz-page-nav.mjs170
-rw-r--r--toolkit/content/widgets/moz-page-nav/moz-page-nav.stories.mjs77
4 files changed, 446 insertions, 0 deletions
diff --git a/toolkit/content/widgets/moz-page-nav/moz-page-nav-button.css b/toolkit/content/widgets/moz-page-nav/moz-page-nav-button.css
new file mode 100644
index 0000000000..2975bb1a7c
--- /dev/null
+++ b/toolkit/content/widgets/moz-page-nav/moz-page-nav-button.css
@@ -0,0 +1,123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+:host {
+ border-radius: var(--border-radius-small);
+ &:focus-visible {
+ outline-offset: var(--page-nav-focus-outline-inset);
+ }
+}
+
+button {
+ background-color: var(--page-nav-button-background-color);
+ border: 1px solid var(--page-nav-border-color);
+ -moz-context-properties: fill, fill-opacity;
+ fill: currentColor;
+ display: grid;
+ grid-template-columns: min-content 1fr;
+ gap: 12px;
+ align-items: center;
+ font-size: inherit;
+ width: 100%;
+ font-weight: normal;
+ border-radius: var(--page-nav-button-border-radius);
+ color: var(--page-nav-button-text-color);
+ text-align: start;
+ transition: background-color 150ms;
+ padding: var(--page-nav-button-padding);
+}
+
+button:hover {
+ cursor: pointer;
+}
+
+@media not (prefers-contrast) {
+ button {
+ border-inline-start: 2px solid transparent;
+ border-inline-end: none;
+ border-block: none;
+ }
+
+ button:hover,
+ button[selected]:hover {
+ background-color: var(--page-nav-button-background-color-hover);
+ }
+
+ button[selected]:hover {
+ border-inline-start-color: inherit;
+ }
+
+ button[selected],
+ button[selected]:hover {
+ border-inline-start: 2px solid;
+ }
+
+ button[selected]:not(:focus-visible) {
+ border-start-start-radius: 0;
+ border-end-start-radius: 0;
+ }
+
+ button[selected]:not(:hover) {
+ color: var(--color-accent-primary);
+ background-color: color-mix(in srgb, var(--page-nav-button-background-color-selected) 5%, transparent);
+ border-inline-start-color: var(--color-accent-primary);
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ button[selected] {
+ background-color: color-mix(in srgb, var(--page-nav-button-background-color-selected) 12%, transparent);
+ }
+}
+
+button:focus-visible,
+button[selected]:focus-visible {
+ outline: var(--focus-outline);
+ outline-offset: var(--focus-outline-offset);
+ border-radius: var(--border-radius-small);
+}
+
+.page-nav-icon {
+ height: 20px;
+ width: 20px;
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+@media (prefers-contrast) {
+ button {
+ transition: none;
+ border-color: ButtonText;
+ background-color: var(--button-background-color);
+ }
+
+ button:hover {
+ color: SelectedItem;
+ }
+
+ button[selected] {
+ color: SelectedItemText;
+ background-color: SelectedItem;
+ border-color: SelectedItem;
+ }
+}
+
+slot {
+ font-size: var(--font-size-large);
+ margin: 0;
+ padding-inline-start: 0;
+ user-select: none;
+}
+
+@media (max-width: 52rem) {
+ button {
+ grid-template-columns: min-content;
+ justify-content: center;
+ margin-inline: 0;
+ }
+
+ slot {
+ display: none;
+ }
+}
diff --git a/toolkit/content/widgets/moz-page-nav/moz-page-nav.css b/toolkit/content/widgets/moz-page-nav/moz-page-nav.css
new file mode 100644
index 0000000000..49000f622d
--- /dev/null
+++ b/toolkit/content/widgets/moz-page-nav/moz-page-nav.css
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+:host {
+ --page-nav-button-border-radius: var(--button-border-radius);
+ --page-nav-button-text-color: var(--button-text-color);
+ --page-nav-button-background-color: transparent;
+ --page-nav-button-background-color-hover: var(--button-background-color-hover);
+ --page-nav-button-background-color-selected: var(--color-accent-primary);
+ --page-nav-button-padding: var(--space-small);
+ --page-nav-margin-top: 72px;
+ --page-nav-margin-bottom: 36px;
+ --page-nav-gap: 25px;
+ --page-nav-button-gap: var(--space-xsmall);
+ --page-nav-border-color: var(--border-color);
+ --page-nav-focus-outline-inset: var(--focus-outline-inset);
+ --page-nav-width: 240px;
+ margin-inline-start: 42px;
+ position: sticky;
+ top: 0;
+ height: 100vh;
+ width: var(--page-nav-width);
+
+ @media (prefers-reduced-motion) {
+ /* (See Bug 1610081) Setting border-inline-end to add clear differentiation between side navigation and main content area */
+ border-inline-end: 1px solid var(--page-nav-border-color);
+ }
+
+ @media (max-width: 52rem) {
+ grid-template-rows: 1fr auto;
+ --page-nav-width: 118px;
+ }
+}
+
+nav {
+ display: grid;
+ grid-template-rows: min-content 1fr auto;
+ gap: var(--page-nav-gap);
+ margin-block: var(--page-nav-margin-top) var(--page-nav-margin-bottom);
+ height: calc(100vh - var(--page-nav-margin-top) - var(--page-nav-margin-bottom));
+ @media (max-width: 52rem) {
+ grid-template-rows: 1fr auto;
+ }
+}
+
+.page-nav-header {
+ /* Align the header text/icon with the page nav button icons */
+ margin-inline-start: var(--page-nav-button-padding);
+ font-size: var(--font-size-xlarge);
+ font-weight: var(--font-weight-bold);
+ margin-block: 0;
+
+ @media (max-width: 52rem) {
+ display: none;
+ }
+}
+
+.primary-nav-group,
+#secondary-nav-group {
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-auto-rows: min-content;
+ gap: var(--page-nav-button-gap);
+
+ @media (max-width: 52rem) {
+ justify-content: center;
+ grid-template-columns: min-content;
+ }
+}
+
+@media (prefers-contrast) {
+ .primary-nav-group {
+ gap: var(--space-small);
+ }
+}
diff --git a/toolkit/content/widgets/moz-page-nav/moz-page-nav.mjs b/toolkit/content/widgets/moz-page-nav/moz-page-nav.mjs
new file mode 100644
index 0000000000..f998ee735f
--- /dev/null
+++ b/toolkit/content/widgets/moz-page-nav/moz-page-nav.mjs
@@ -0,0 +1,170 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { html } from "chrome://global/content/vendor/lit.all.mjs";
+import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
+
+/**
+ * A grouping of navigation buttons that is displayed at the page level,
+ * intended to change the selected view, provide a heading, and have links
+ * to external resources.
+ *
+ * @tagname moz-page-nav
+ * @property {string} currentView - The currently selected view.
+ * @property {string} heading - A heading to be displayed at the top of the navigation.
+ * @slot [default] - Used to append moz-page-nav-button elements to the navigation.
+ */
+export default class MozPageNav extends MozLitElement {
+ static properties = {
+ currentView: { type: String },
+ heading: { type: String },
+ };
+
+ static queries = {
+ headingEl: "#page-nav-header",
+ primaryNavGroupSlot: ".primary-nav-group slot",
+ secondaryNavGroupSlot: "#secondary-nav-group slot",
+ };
+
+ get pageNavButtons() {
+ return this.primaryNavGroupSlot
+ .assignedNodes()
+ .filter(
+ node => node?.localName === "moz-page-nav-button" && !node.hidden
+ );
+ }
+
+ onChangeView(e) {
+ this.currentView = e.target.view;
+ }
+
+ handleFocus(e) {
+ if (e.key == "ArrowDown" || e.key == "ArrowRight") {
+ e.preventDefault();
+ this.focusNextView();
+ } else if (e.key == "ArrowUp" || e.key == "ArrowLeft") {
+ e.preventDefault();
+ this.focusPreviousView();
+ }
+ }
+
+ focusPreviousView() {
+ let pageNavButtons = this.pageNavButtons;
+ let currentIndex = pageNavButtons.findIndex(b => b.selected);
+ let prev = pageNavButtons[currentIndex - 1];
+ if (prev) {
+ prev.activate();
+ prev.buttonEl.focus();
+ }
+ }
+
+ focusNextView() {
+ let pageNavButtons = this.pageNavButtons;
+ let currentIndex = pageNavButtons.findIndex(b => b.selected);
+ let next = pageNavButtons[currentIndex + 1];
+ if (next) {
+ next.activate();
+ next.buttonEl.focus();
+ }
+ }
+
+ render() {
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://global/content/elements/moz-page-nav.css"
+ />
+ <nav>
+ <h1 class="page-nav-header" id="page-nav-header">${this.heading}</h1>
+ <div
+ class="primary-nav-group"
+ role="tablist"
+ aria-orientation="vertical"
+ aria-labelledby="page-nav-header"
+ >
+ <slot
+ @change-view=${this.onChangeView}
+ @keydown=${this.handleFocus}
+ ></slot>
+ </div>
+ <div id="secondary-nav-group" role="group">
+ <slot name="secondary-nav" @keydown=${this.handleFocus}></slot>
+ </div>
+ </nav>
+ `;
+ }
+
+ updated() {
+ let isViewSelected = false;
+ let assignedPageNavButtons = this.pageNavButtons;
+ for (let button of assignedPageNavButtons) {
+ button.selected = button.view == this.currentView;
+ isViewSelected = isViewSelected || button.selected;
+ }
+ if (!isViewSelected && assignedPageNavButtons.length) {
+ // Current page nav has no matching view, reset to the first view.
+ assignedPageNavButtons[0].activate();
+ }
+ }
+}
+customElements.define("moz-page-nav", MozPageNav);
+
+/**
+ * A navigation button intended to change the selected view within a page.
+ *
+ * @tagname moz-page-nav-button
+ * @property {string} iconSrc - The chrome:// url for the icon used for the button.
+ * @property {string} l10nId - The fluent ID for the button's text
+ * @property {boolean} selected - Whether or not the button is currently selected.
+ * @slot [default] - Used to append the l10n string to the button.
+ */
+export class MozPageNavButton extends MozLitElement {
+ static properties = {
+ iconSrc: { type: String },
+ l10nId: { type: String },
+ selected: { type: Boolean },
+ };
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.setAttribute("role", "none");
+ }
+
+ static queries = {
+ buttonEl: "button",
+ };
+
+ get view() {
+ return this.getAttribute("view");
+ }
+
+ activate() {
+ this.dispatchEvent(
+ new CustomEvent("change-view", {
+ bubbles: true,
+ composed: true,
+ })
+ );
+ }
+
+ render() {
+ return html`
+ <link
+ rel="stylesheet"
+ href="chrome://global/content/elements/moz-page-nav-button.css"
+ />
+ <button
+ aria-selected=${this.selected}
+ tabindex=${this.selected ? 0 : -1}
+ role="tab"
+ ?selected=${this.selected}
+ @click=${this.activate}
+ >
+ <img class="page-nav-icon" src=${this.iconSrc} />
+ <slot></slot>
+ </button>
+ `;
+ }
+}
+customElements.define("moz-page-nav-button", MozPageNavButton);
diff --git a/toolkit/content/widgets/moz-page-nav/moz-page-nav.stories.mjs b/toolkit/content/widgets/moz-page-nav/moz-page-nav.stories.mjs
new file mode 100644
index 0000000000..4ac7b455cf
--- /dev/null
+++ b/toolkit/content/widgets/moz-page-nav/moz-page-nav.stories.mjs
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { html } from "../vendor/lit.all.mjs";
+// eslint-disable-next-line import/no-unassigned-import
+import "./moz-page-nav.mjs";
+
+export default {
+ title: "UI Widgets/Page Nav",
+ component: "moz-page-nav",
+ parameters: {
+ status: "in-development",
+ actions: {
+ handles: ["change-view"],
+ },
+ fluent: `
+moz-page-nav-button-one = View 1
+ .title = View 1
+moz-page-nav-button-two = View 2
+ .title = View 2
+moz-page-nav-button-three = View 3
+ .title = View 3
+moz-page-link-one = Support Page
+ .title = Support Page
+moz-page-nav-heading =
+ .heading = Heading
+ `,
+ },
+};
+
+const Template = () => html`
+ <style>
+ #page {
+ height: 100%;
+ display: flex;
+
+ @media (max-width: 52rem) {
+ grid-template-columns: 82px 1fr;
+ }
+ }
+ moz-page-nav {
+ margin-inline-start: 10px;
+ --page-nav-margin-top: 10px;
+
+ @media (max-width: 52rem) {
+ margin-inline-start: 0;
+ }
+ }
+ </style>
+ <div id="page">
+ <moz-page-nav data-l10n-id="moz-page-nav-heading" data-l10n-attrs="heading">
+ <moz-page-nav-button
+ view="view-one"
+ data-l10n-id="moz-page-nav-button-one"
+ iconSrc="chrome://browser/skin/preferences/category-general.svg"
+ >
+ </moz-page-nav-button>
+ <moz-page-nav-button
+ view="view-two"
+ data-l10n-id="moz-page-nav-button-two"
+ iconSrc="chrome://browser/skin/preferences/category-general.svg"
+ >
+ </moz-page-nav-button>
+ <moz-page-nav-button
+ view="view-three"
+ data-l10n-id="moz-page-nav-button-three"
+ iconSrc="chrome://browser/skin/preferences/category-general.svg"
+ >
+ </moz-page-nav-button>
+ </moz-page-nav>
+ <main></main>
+ </div>
+`;
+
+export const Default = Template.bind({});
+Default.args = {};