@openagenda/react-filters
v3.2.0
Published
This library works with OpenAgenda-compatible query structures. It provides a controller that loads calendar filters in designated placeholders presented on a webpage. It is used on the [OpenAgenda](https://openagenda.com) platform as well as by the follo
Readme
@openagenda/react-filters
This library works with OpenAgenda-compatible query structures. It provides a controller that loads calendar filters in designated placeholders presented on a webpage. It is used on the OpenAgenda platform as well as by the following libraries:
- The OpenAgenda Wordpress plugin
- The OpenAgenda Drupal plugin
- The OpenAgenda agenda-portal event portal library
Table of contents:
Principles
Configuration is provided to the controller through a window.oa object that must be initialized before the filter library is loaded.
<html>
<head>
<!-- after your own framework stylesheet, see Styles below -->
<link
rel="stylesheet"
href="https://unpkg.com/@openagenda/[email protected]/dist/style.css"
/>
</head>
<body>
<!-- place filter <div /> anchor points in your webpage -->
<script>
window.oa = {
res: '/events',
locale: 'fr',
onLoad: () => {}, // ...
onFilterChange: (), // ...
// ...
};
</script>
<script src="https://unpkg.com/@openagenda/[email protected]/dist/main.js"></script>
</body>
</html>The OpenAgenda plugins load this object with the configuration provided in their backend. When used, initializing the window.oa separately should not be done. In this case, your focus should be on the filters part of this documentation.
If you need to use this library outside of the context of a plugin, see the contents of the example folder of the repo for details on the parameters to be provided to the window.oa object. In that case, requests can be sent directly to the OpenAgenda API endpoints
Controller options
This are loaded through the window.oa object.
- res: The path where the controller will retrieve aggregated data to populate the filters with
- locale: The locale in which the filters should be loaded
- locales: Optional. The filter labels to be loaded over defaults. Use the following as base:
https://github.com/OpenAgenda/oa-public/blob/main/react-filters/src/locales/fr.json - aggregations: Optional. The aggregated data object to be used by the controller at initialisation.
- total: The current total displayed events
- defaultViewport: Viewport to be used when no results are found. Format:
{ bottomRight: { latitude, longitude }, topLeft: { latitude, longitude } } - query: Optional. Filter value to be loaded at initialization.
- onFilterChange: The function to be called each time a filter value changes. This function should be in charge of loading the matching events in the list and updating the URL.
- onLoad: This function is called when the controller has been loaded.
- manualSubmit: Optional,
falseby default. Whentrue, filter changes are no longer applied as they happen: they are held in the form until it is submitted, and only then isonFilterChangecalled. The page provides the submit control. See Manual submit. - onPendingChange: Optional. Called as
onPendingChange(pending, values)when the filters start or stop holding changes that have not been applied, in manual submit mode. See Manual submit. - apiClient: An optional axios instance. Useful for tests.
- ref: A React ref. Useful if the controller is to be used with the
FilterManagercomponent (not documented). SeeReact.createRef()orReact.useRef()
Styles
Since 3.0.0, the library ships its rules as a stylesheet you load yourself. Earlier versions injected them at runtime, so nothing had to be loaded — an integration upgrading from 2.x renders unstyled filters until the stylesheet is added.
When the library is loaded through a <script> tag, add the matching <link>:
<link
rel="stylesheet"
href="https://unpkg.com/@openagenda/[email protected]/dist/style.css"
/>When it is consumed by a bundler, import it once — anywhere in the entry point of the application:
import '@openagenda/react-filters/style.css';When your application already has a sass entry point, importing it there is usually the better place — it puts the rules where you can see and reorder them. Import it without the extension, so sass inlines the file instead of emitting a plain CSS @import:
@import '@openagenda/react-filters/style';Load it after the framework stylesheet the filters sit on (bootstrap, in every integration we know of). Rules such as the value badge padding or the choice search width are written with a single class, so they overrule .btn, .badge and .form-control by source order rather than by specificity — loading them before bootstrap gives those defaults back.
The class names the components carry are part of the public API and are meant to be targeted from your own stylesheet: oa-filters-value-badge, oa-filters-choice-search, oa-filters-map, oa-filters-map-container, oa-filters-search-here, oa-filters-search-here-button, oa-filters-search-label, and for the sort widget menu oa-filters-sort-option, oa-filters-sort-label and oa-filters-sort-description. Any rule of yours placed after the stylesheet overrides ours.
Note that the map filter also needs leaflet's own stylesheet, which this library does not bundle.
Filters
A filter is defined by placing a div with data-oa-filter and data-oa-filter-params attributes in the webpage. At page load, the controler scans for any divs containing those attributes and attempts to load the corresponding filter.
data-oa-filtershould contain a unique identifier for the filter on the site where it is loadeddata-oa-filter-paramsshould container the parameters for the filter encoded in JSON and escaped (ex{"name": "city"}becomes{"name":"city"}).
Most filters can be loaded using the following basic configuration (here, a filter that provides a list of cities from which the user can filter the event selection to focus on the requested cities):
<div
data-oa-filter="unique-filter-id"
data-oa-filter-params="{"name":"city"}"
></div>Here is a non-comprehensive list of the filters that can be loaded (to be placed in the name parameter):
search: A search text input.map: A map for filtering based on events geolocation data. See below for details.locationUid: Loads a list of location names.city: as shown in the example. Provides a list of cities.region: Loads a list of regions (equivalent of administrative level 1)department: Loads a list of department (equivalent of administrative level 2)adminLevel3: Loads a list of city groups (administrative level 3)city: Loads a list of cities.keyword: Loads a list of keywords.accessibility: Loads a list of accessibility installation filters.attendanceMode: Loads a filter to filter fromonline,offlineormixedevents.featured: Filter between featured and non-featured events.relative: Filter between passed, ongoing and upcoming eventstimings: Loads a calendar picker when the type isdateRangeand range links (today, tomorrow, this weekend, ...) when the type isdefinedRange. See below for details.
For all filters that take the form of a list of checkboxes, the parameter inputType can be set to radio if the required behavior is to limit filtering to one value at a time per field.
### Search
A text input for open-text search. Parameters are:
- name: 'search'.
- manualSearch: Boolean, false by default. When true, search is only triggered when the user submits the value.
- label: Text of a
<label>rendered above the input and associated with it. It carries the classoa-filters-search-label. Without it, the input is only named by itsaria-label. - placeholder: Placeholder of the input. Defaults to "Search", translated.
Map
The map filter displays a map at the location of the container div that will allow the user to focus the selection of events according to their geographical locations. Parameters are:
- name: 'geo'. Tells the controller the map filter is to be loaded.
- mapClass: The class to be given to the map component container div.
- tileUrl: The tiles to be used. Ex:
//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png - tileAttribution: The attribution for the tiles. Ex:
Map data © OpenStreetMap contributors - searchWithMap: Boolean. Whether the event selection should be reloaded as the user navigates through the map.
- searchMessage: Text to be displayed next to the checkbox controlling whether the selection should be reloaded with map navigation.
Timings
The timings filter displays a calendar or range links that allow the user to filter the event selection to those occuring during the selected time range.
- name: 'timings'
- type: 'dateRange' for the calendar, 'simpleDateRange' for native date inputs (see Accessibility), 'definedRange' for predefined range links (today, tomorrow, this weekend)
- staticRanges: when the type is 'definedRange', specifies which ranges to be displayed. Possible values are
['today', 'tomorrow', 'thisWeekend', 'currentWeek', 'currentMonth'] - dateFormat: Change the formatting of dates displayed in the from and to input fields. By default, uses date-fns synthax by default .
- dateFormatStyle: date-fns by default. When the value is set to "php", reads the 'dateFormat' configuration in a php formatting style
Explicit filter options
It is possible to restrict displayed options of a given filter by providing an explicit list of options to its parameters. In the following example, only the "true" value will be displayed. Labels can also be set in the options object:
<div
data-oa-filter="evenement-jeune-public"
data-oa-filter-params="<%= JSON.stringify({
type: 'choice',
name: 'audience-type',
options: [
{
label: 'Youth',
value: 'true'
}
],
aggregation: {
type: 'additionalFields',
field: 'audience-type',
},
}) %>"
></div>The HTML equivalent:
<div
data-oa-filter="evenement-jeune-public"
data-oa-filter-params="{"type":"choice","name":"evenement-jeune-public","options":[{"label":"Jeune public","value":"true"},{"label":"Vieux public","value":"false"}],"aggregation":{"type":"additionalFields","field":"audience-type"}}"
></div>Widgets
A widget is placed like a filter, with data-oa-widget and data-oa-widget-params attributes, the name parameter telling which widget to load. Widgets act on the event list rather than narrowing it.
Sort
A select that sets the order of the event list. Its value is the sort key of the values passed to onFilterChange, in the format of the OpenAgenda API sort parameter.
<div
data-oa-widget="sort"
data-oa-widget-params='{"name":"sort","options":["lastTimingWithFeatured.asc","updatedAt.desc"],"defaultValue":"lastTimingWithFeatured.asc"}'
></div>Parameters are:
- name: 'sort'.
- options: Optional. The orders offered, listed in the order given. Defaults to
["lastTimingWithFeatured.asc", "score"]. Offer a single chronological order: their short labels are the same. The select is hidden while it has a single choice, as with the defaults until a search adds relevance. Values outside the list below are ignored. - defaultValue: Optional. The order shown while the visitor has not picked one. Falls back to the first option when missing or not among the options.
- labels: Optional. Per-value label overrides, ex
{"lastTimingWithFeatured.asc":"Upcoming"}. The label is what the menu lists. The closed select shows it too when the page sets it, else the default short label. Without it, each order is labelled by what it does, in the controller locale. - label: Optional. Accessible name of the select, read by screen readers. Defaults to "Sort by", translated. The select shows no visible label: set this when the page around it gives the list a more specific name.
- shortLabels: Optional. Per-value labels for the closed select only, ex
{"updatedAt.desc":"Latest"}. The menu keeps the full label. Without it, the closed select shows the label given inlabels, else the default short label below. - descriptions: Optional. Per-value detail lines, shown under each label in the open menu only. Overrides the default ones below, ex
{"lastTimingWithFeatured.asc":"Highlighted events first"}.falseshows labels only.
The available orders:
| Value | Default short label | Default label | Default description |
| ---------------------------- | ------------------- | ----------------------------------------- | ---------------------------------------------------------- |
| lastTimingWithFeatured.asc | Chronological | Featured first, chronological (last date) | Featured events first, then by the last date of each event |
| timingsWithFeatured.asc | Chronological | Featured first, chronological | Featured events first, then by next date |
| lastTiming.asc | Chronological | Chronological (last date) | By the last date of each event |
| timings.asc | Chronological | Chronological order | Next dates first |
| updatedAt.desc | Recently updated | Recently updated | Last modified events first |
| updatedAt.asc | Oldest updates | Least recently updated | Least recently modified events first |
| score | Relevance | Relevance | Best matches for the search first |
timings orders events by their next date, lastTiming by their last one. The WithFeatured variants list featured events first.
score is only listed while a search is typed, since relevance ranks against it. When it is among the options, typing a search switches the order to relevance, and clearing the search brings back the order picked before.
The widget starts on the sort of the initial query, so a link carrying an order shows it and keeps it through later filter changes. An order the widget does not offer is still shown, under its label. Without one, the values passed to onFilterChange carry no sort until the visitor picks an order: the page should then request the events in the order given as defaultValue.
Accessibility
Total widget: role="status"
Add role="status" on the container div of the total widget so that screen readers announce the updated count when filters change:
<div role="status" data-oa-widget="total" data-oa-widget-params="..."></div>This makes the element a live region (aria-live="polite" is implicit), so assistive technologies will announce the new total without interrupting the user.
Manual submit
With manualSubmit, a visitor can change the filters without the list following, so that nothing reloads until they ask for it. Between a change and the next submit, the filters show one selection while the list, the total and the URL still show the one last applied: the filters are pending.
onPendingChange(pending, values) reports that state so the page can say so:
pendingistruewhen the filter values differ from the applied ones,falsewhen they match again, whether the visitor submitted or undid their changes. The applied values are those of the last submit, or the initialquerybefore any submit.- It is called on transitions only, never twice in a row with the same
pending, so it does not follow every keystroke in the search field. It is not called at load, where the filters are not pending. valuesare the filter values at the time of the transition.- In automatic mode (no
manualSubmit) every change is applied at once and the callback is never called.
react-filters does not render the list, the total or the submit button, so the page renders the cue. For instance, fill a live region placed in the page at load, and emphasise the submit button:
<div role="status" id="filters-pending"></div>
<button type="submit" aria-describedby="filters-pending">Search</button>
<script>
window.oa = {
// ...
manualSubmit: true,
onPendingChange: (pending) => {
document.getElementById('filters-pending').textContent = pending
? 'The results do not match the selected filters yet.'
: '';
document
.querySelector('button[type=submit]')
.classList.toggle('btn-primary', pending);
},
};
</script>Timings: simpleDateRange type
Use simpleDateRange instead of dateRange for an accessible date filter. It renders native <input type="date"> elements with proper <label> associations instead of the custom calendar picker:
<div
data-oa-filter="date-range"
data-oa-filter-params='{"type":"simpleDateRange","name":"timings"}'
></div>Native date inputs provide built-in keyboard navigation and screen reader support from the browser.
Choice filters: tag parameter
By default, each choice option is wrapped in a <div>. Set tag to "label" to wrap each option in a <label> element, creating a proper semantic association between the text and the checkbox/radio input:
<div
data-oa-filter="city"
data-oa-filter-params='{"name":"city","tag":"label"}'
></div>