Skip to content

Commit

Permalink
feat(demo): nav:back element (#1110)
Browse files Browse the repository at this point in the history
Add new element to render the back or close button conditionally, based
on the screen's navigation context.



https://github.com/user-attachments/assets/c53d1c2b-16fe-4ac1-a515-85a030ec6e09

---------

Co-authored-by: flochtililoch <flochtililoch@gmail.com>
Co-authored-by: Hardin Gray <hgray@instawork.com>
  • Loading branch information
3 people authored Mar 4, 2025
1 parent 7255fa7 commit fb13837
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 46 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<style
id="text"
marginHorizontal="24"
marginBottom="16"
/>
26 changes: 26 additions & 0 deletions demo/backend/advanced/community/elements/nav-back/index.xml.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
permalink: "/backend/advanced/community/nav-back/index.xml"
tags: "Advanced/Community/Elements"
hv_button_behavior: "back"
hv_title: "Nav Back"
---

{% from 'macros/button/index.xml.njk' import button %}
{% from 'macros/description/index.xml.njk' import description %}
{% extends 'templates/scrollview.xml.njk' %}

{% block content %}
{{ description('Tapping one of the buttons will render the same screen with a different navigation style. The control to navigate back will be dynamic, based on the navigation context.') }}
{% call button('Push') -%}
<behavior
action="push"
href="/hyperview/public/advanced/community/elements/nav-back/screen.xml"
/>
{%- endcall %}
{% call button('New') -%}
<behavior
action="new"
href="/hyperview/public/advanced/community/elements/nav-back/screen.xml"
/>
{%- endcall %}
{% endblock %}
44 changes: 44 additions & 0 deletions demo/backend/advanced/community/elements/nav-back/screen.xml.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
permalink: "/backend/advanced/community/nav-back/screen.xml"
hv_title: "Nav Back"
---

{% from 'macros/button/index.xml.njk' import button %}
{% from 'macros/description/index.xml.njk' import description %}
{% extends 'templates/base.xml.njk' %}

{% block styles %}
{% include './_styles.xml.njk' %}
{% endblock %}

{% block body %}
<header style="header">
<nav:back xmlns:nav="https://hyperview.org/navigation">
<view
nav:role="close"
action="close"
href="#"
style="header-btn"
>
{% include 'icons/close.svg' %}
</view>
<view
nav:role="back"
action="back"
href="#"
style="header-btn"
>
{% include 'icons/back.svg' %}
</view>
</nav:back>
<text style="header-title">{{ hv_title }}</text>
</header>
<text style="text">
The button to navigate back to the previous screen will be rendered dynamically,
based on which kind of navigator the screen is currently loaded on. For regular screens
(added to the stack via "push" action), a back-arrow button will be rendered. For
modal screens (added to the stack via "new" action), a close button will be rendered.
Also (not demo'd here), if the screen is the first screen of the stack and there is no
place to navigate back to, no button will be rendered.
</text>
{% endblock %}
2 changes: 2 additions & 0 deletions demo/schema/hyperview.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
xmlns:hv="https://hyperview.org/hyperview"
xmlns:alert="https://hyperview.org/hyperview-alert"
xmlns:filter="https://hyperview.org/filter"
xmlns:nav="https://hyperview.org/navigation"
xmlns:scroll="https://hyperview.org/hyperview-scroll"
xmlns:share="https://hyperview.org/share"
>
Expand Down Expand Up @@ -85,6 +86,7 @@

<!-- Hyperview demo extensions -->
<xs:attributeGroup ref="filter:filterAttributes" />
<xs:attributeGroup ref="nav:navAttributes" />
<xs:attributeGroup ref="share:shareAttributes" />
</xs:attributeGroup>

Expand Down
17 changes: 17 additions & 0 deletions demo/schema/navigation.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
attributeFormDefault="qualified"
elementFormDefault="qualified"
targetNamespace="https://hyperview.org/navigation"
xmlns:nav="https://hyperview.org/navigation"
xmlns:hv="https://hyperview.org/hyperview"
>
<xs:import
Expand All @@ -28,4 +29,20 @@
<xs:attribute name="key" type="hv:KEY" form="unqualified" />
</xs:complexType>
</xs:element>
<xs:simpleType name="role">
<xs:restriction base="xs:string">
<xs:enumeration value="back" />
<xs:enumeration value="close" />
</xs:restriction>
</xs:simpleType>
<xs:element name="back">
<xs:complexType>
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:attributeGroup name="navAttributes">
<xs:attribute name="role" type="nav:role" />
</xs:attributeGroup>
</xs:schema>
66 changes: 20 additions & 46 deletions demo/src/Components/Filter/Filter.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import * as Logging from 'hyperview/src/services/logging';
import type { HvComponentProps, LocalName } from 'hyperview';
import Hyperview, {
Events,
LOCAL_NAME,
NODE_TYPE,
Namespaces,
} from 'hyperview';
import Hyperview, { Events, LOCAL_NAME, Namespaces } from 'hyperview';
import { findElements } from '../../Helpers';
import { useEffect } from 'react';

type FormDataPart = {
Expand All @@ -18,30 +14,6 @@ declare class FormData {
}
const FILTER_NS = 'https://hyperview.org/filter';

export const findElements = (node: Element, attributeNames: string[]) => {
if (node.nodeType !== NODE_TYPE.ELEMENT_NODE) {
return [];
}

if (
attributeNames.reduce(
(found, name) => found || !!node.getAttributeNS(FILTER_NS, name),
false,
)
) {
return [node];
}

return (Array.from(node.childNodes) as Element[])
.filter((child: Node | null) => {
return child !== null && child.nodeType === NODE_TYPE.ELEMENT_NODE;
})
.reduce((elements: Element[], child: Element) => {
elements.push(...findElements(child, attributeNames));
return elements;
}, []);
};

const Filter = (props: HvComponentProps) => {
const onEventDispatch = (eventName: string) => {
const filterEvent =
Expand Down Expand Up @@ -74,7 +46,7 @@ const Filter = (props: HvComponentProps) => {
: filterTerm;

// Hide/show each element with filter terms or matching given regex. Modify attributes in-place
const filterElements: Element[] = findElements(props.element, [
const filterElements: Element[] = findElements(FILTER_NS, props.element, [
'terms',
'regex',
]);
Expand All @@ -101,22 +73,24 @@ const Filter = (props: HvComponentProps) => {
props.onUpdate(null, 'swap', props.element, { newElement });

// Set elements that need to render the filter term
findElements(newElement, ['role']).forEach((element: Element) => {
if (element.getAttributeNS(FILTER_NS, 'role') === 'filter-terms') {
if (
element.namespaceURI === Namespaces.HYPERVIEW &&
element.localName !== LOCAL_NAME.TEXT
) {
Logging.error(
'Element with attribute `role="filter-terms"` should be a <text> element or a custom element',
);
return;
findElements(FILTER_NS, newElement, ['role']).forEach(
(element: Element) => {
if (element.getAttributeNS(FILTER_NS, 'role') === 'filter-terms') {
if (
element.namespaceURI === Namespaces.HYPERVIEW &&
element.localName !== LOCAL_NAME.TEXT
) {
Logging.error(
'Element with attribute `role="filter-terms"` should be a <text> element or a custom element',
);
return;
}
const newRoleElement = element.cloneNode(true) as Element;
newRoleElement.textContent = filterTerm;
props.onUpdate(null, 'swap', element, { newElement: newRoleElement });
}
const newRoleElement = element.cloneNode(true) as Element;
newRoleElement.textContent = filterTerm;
props.onUpdate(null, 'swap', element, { newElement: newRoleElement });
}
});
},
);
};

useEffect(() => {
Expand Down
40 changes: 40 additions & 0 deletions demo/src/Components/NavBack/NavBack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { HvComponentProps, LocalName } from 'hyperview';
import Hyperview from 'hyperview';
import { NavigationContext } from '@react-navigation/native';
import { findElements } from '../../Helpers';
import { useContext } from 'react';

export const namespaceURI = 'https://hyperview.org/navigation';

const NavBack = (props: HvComponentProps) => {
const ctx = useContext(NavigationContext);
const state = ctx?.getState();
const route = state?.routes[state.index];

// Screen is first on the stack? Don't render anything
if (state?.index === 0) {
return null;
}

const role = route?.name === 'modal' ? 'close' : 'back';
const [element] = findElements(namespaceURI, props.element, ['role']).filter(
el => {
return el.getAttributeNS(namespaceURI, 'role') === role;
},
);
if (!element) {
return null;
}
return (Hyperview.renderElement(
element,
props.stylesheets,
props.onUpdate,
props.options,
) as unknown) as JSX.Element;
};

NavBack.namespaceURI = namespaceURI;
NavBack.localName = 'back' as LocalName;
NavBack.localNameAliases = [] as LocalName[];

export { NavBack };
1 change: 1 addition & 0 deletions demo/src/Components/NavBack/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { NavBack } from './NavBack';
2 changes: 2 additions & 0 deletions demo/src/Components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
import { BottomTabBar, BottomTabBarItem } from './BottomTabBar';
import { Map, MapMarker } from './Map';
import { Filter } from './Filter';
import { NavBack } from './NavBack';
import { ProgressBar } from './ProgressBar';
import { ScrollOpacity } from './ScrollOpacity';
import { Svg } from './Svg';
Expand All @@ -19,6 +20,7 @@ export default [
Filter,
Map,
MapMarker,
NavBack,
ProgressBar,
ScrollOpacity,
Svg,
Expand Down
29 changes: 29 additions & 0 deletions demo/src/Helpers/misc.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { NODE_TYPE } from 'hyperview';
import moment from 'moment';

export const formatDate = (
Expand All @@ -21,3 +22,31 @@ export const fetchWrapper = (
mode: 'cors',
});
};

export const findElements = (
namespace: string,
node: Element,
attributeNames: string[],
) => {
if (node.nodeType !== NODE_TYPE.ELEMENT_NODE) {
return [];
}

if (
attributeNames.reduce(
(found, name) => found || !!node.getAttributeNS(namespace, name),
false,
)
) {
return [node];
}

return (Array.from(node.childNodes) as Element[])
.filter((child: Node | null) => {
return child !== null && child.nodeType === NODE_TYPE.ELEMENT_NODE;
})
.reduce((elements: Element[], child: Element) => {
elements.push(...findElements(namespace, child, attributeNames));
return elements;
}, []);
};

0 comments on commit fb13837

Please sign in to comment.