1
0
mirror of https://github.com/matrix-org/matrix-react-sdk.git synced 2025-07-28 15:22:05 +03:00

Apply prettier formatting

This commit is contained in:
Michael Weimann
2022-12-12 12:24:14 +01:00
parent 1cac306093
commit 526645c791
1576 changed files with 65385 additions and 62478 deletions

View File

@ -64,8 +64,8 @@ the content string, caret nodes need to be ignored, as they would confuse the mo
As part of the reconciliation, the caret position is also adjusted to any changes
the model made to the input. The caret is passed around in two formats.
The model receives the caret *offset* within the content string (which includes
The model receives the caret _offset_ within the content string (which includes
an atNodeEnd flag to make it unambiguous if it is at a part and or the next part start).
The model converts this to a caret *position* internally, which has a partIndex
The model converts this to a caret _position_ internally, which has a partIndex
and an offset within the part text, which is more natural to work with.
From there on, the caret *position* is used, also during reconciliation.
From there on, the caret _position_ is used, also during reconciliation.

View File

@ -1,14 +1,17 @@
# Cypress in Element Web
## Scope of this Document
This doc is about our Cypress tests in Element Web and how we use Cypress to write tests.
It aims to cover:
* How to run the tests yourself
* How the tests work
* How to write great Cypress tests
* Visual testing
- How to run the tests yourself
- How the tests work
- How to write great Cypress tests
- Visual testing
## Running the Tests
Our Cypress tests run automatically as part of our CI along with our other tests,
on every pull request and on every merge to develop & master.
@ -43,6 +46,7 @@ yarn run test:cypress:open
```
## How the Tests Work
Everything Cypress-related lives in the `cypress/` subdirectory of react-sdk
as is typical for Cypress tests. Likewise, tests live in `cypress/e2e`.
@ -68,6 +72,7 @@ with each instance in a separate directory named after its ID. These logs are re
at the start of each test run.
## Writing Tests
Mostly this is the same advice as for writing any other Cypress test: the Cypress
docs are well worth a read if you're not already familiar with Cypress testing, eg.
https://docs.cypress.io/guides/references/best-practices. To avoid your tests being
@ -75,11 +80,12 @@ flaky it is also recommended to give https://docs.cypress.io/guides/core-concept
a read.
### Getting a Synapse
The key difference is in starting Synapse instances. Tests use this plugin via
The key difference is in starting Synapse instances. Tests use this plugin via
`cy.startSynapse()` to provide a Synapse instance to log into:
```javascript
cy.startSynapse("consent").then(result => {
cy.startSynapse("consent").then((result) => {
synapse = result;
});
```
@ -96,32 +102,38 @@ Synapse instance for each test suite, i.e. in `before()`, and then tear it down
To later destroy your Synapse you should call `stopSynapse`, passing the SynapseInstance
object you received when starting it.
```javascript
cy.stopSynapse(synapse);
```
### Synapse Config Templates
When a Synapse instance is started, it's given a config generated from one of the config
templates in `cypress/plugins/synapsedocker/templates`. There are a couple of special files
in these templates:
* `homeserver.yaml`:
Template substitution happens in this file. Template variables are:
* `REGISTRATION_SECRET`: The secret used to register users via the REST API.
* `MACAROON_SECRET_KEY`: Generated each time for security
* `FORM_SECRET`: Generated each time for security
* `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at
* `localhost.signing.key`: A signing key is auto-generated and saved to this file.
Config templates should not contain a signing key and instead assume that one will exist
in this file.
- `homeserver.yaml`:
Template substitution happens in this file. Template variables are:
- `REGISTRATION_SECRET`: The secret used to register users via the REST API.
- `MACAROON_SECRET_KEY`: Generated each time for security
- `FORM_SECRET`: Generated each time for security
- `PUBLIC_BASEURL`: The localhost url + port combination the synapse is accessible at
- `localhost.signing.key`: A signing key is auto-generated and saved to this file.
Config templates should not contain a signing key and instead assume that one will exist
in this file.
All other files in the template are copied recursively to `/data/`, so the file `foo.html`
in a template can be referenced in the config as `/data/foo.html`.
### Logging In
There exists a basic utility to start the app with a random user already logged in:
```javascript
cy.initTestUser(synapse, "Jeff");
```
It takes the SynapseInstance you received from `startSynapse` and a display name for your test user.
This custom command will register a random userId using the registrationSecret with a random password
and the given display name. The returned Chainable will contain details about the credentials for if
@ -132,20 +144,24 @@ The internals of how this custom command run may be swapped out later,
but the signature can be maintained for simpler maintenance.
### Joining a Room
Many tests will also want to start with the client in a room, ready to send & receive messages. Best
way to do this may be to get an access token for the user and use this to create a room with the REST
API before logging the user in. You can make use of `cy.getBot(synapse)` and `cy.getClient()` to do this.
### Convenience APIs
We should probably end up with convenience APIs that wrap the synapse creation, logging in and room
creation that can be called to set up tests.
### Using matrix-js-sdk
Due to the way we run the Cypress tests in CI, at this time you can only use the matrix-js-sdk module
exposed on `window.matrixcs`. This has the limitation that it is only accessible with the app loaded.
This may be revisited in the future.
## Good Test Hygiene
This section mostly summarises general good Cypress testing practice, and should not be news to anyone
already familiar with Cypress.
@ -158,11 +174,11 @@ already familiar with Cypress.
1. Avoid explicit waits. `cy.get()` will implicitly wait for the specified element to appear and
all assertions are retired until they either pass or time out, so you should never need to
manually wait for an element.
* For example, for asserting about editing an already-edited message, you can't wait for the
- For example, for asserting about editing an already-edited message, you can't wait for the
'edited' element to appear as there was already one there, but you can assert that the body
of the message is what is should be after the second edit and this assertion will pass once
it becomes true. You can then assert that the 'edited' element is still in the DOM.
* You can also wait for other things like network requests in the
- You can also wait for other things like network requests in the
browser to complete (https://docs.cypress.io/guides/guides/network-requests#Waiting).
Needing to wait for things can also be because of race conditions in the app itself, which ideally
shouldn't be there!
@ -171,6 +187,7 @@ This is a small selection - the Cypress best practices guide, linked above, has
should generally try to adhere to them.
## Percy Visual Testing
We also support visual testing via Percy, this extracts the DOM from Cypress and renders it using custom renderers
for Safari, Firefox, Chrome & Edge, allowing us to spot visual regressions before they become release regressions.
Each `cy.percySnapshot()` call results in 8 screenshots (4 browsers, 2 sizes) this can quickly be exhausted and
@ -178,4 +195,3 @@ so we only run Percy testing on `develop` and PRs which are labelled `X-Needs-Pe
To record a snapshot use `cy.percySnapshot()`, you may have to pass `percyCSS` into the 2nd argument to hide certain
elements which contain dynamic/generated data to avoid them cause false positives in the Percy screenshot diffs.

View File

@ -1,37 +1,38 @@
# Composer Features
## Auto Complete
- Hitting tab tries to auto-complete the word before the caret as a room member
- If no matching name is found, a visual bell is shown
- @ + a letter opens auto complete for members starting with the given letter
- When inserting a user pill at the start in the composer, a colon and space is appended to the pill
- When inserting a user pill anywhere else in composer, only a space is appended to the pill
- # + a letter opens auto complete for rooms starting with the given letter
- : open auto complete for emoji
- Pressing arrow-up/arrow-down while the autocomplete is open navigates between auto complete options
- Pressing tab while the autocomplete is open goes to the next autocomplete option,
- Hitting tab tries to auto-complete the word before the caret as a room member
- If no matching name is found, a visual bell is shown
- @ + a letter opens auto complete for members starting with the given letter
- When inserting a user pill at the start in the composer, a colon and space is appended to the pill
- When inserting a user pill anywhere else in composer, only a space is appended to the pill
- # + a letter opens auto complete for rooms starting with the given letter
- : open auto complete for emoji
- Pressing arrow-up/arrow-down while the autocomplete is open navigates between auto complete options
- Pressing tab while the autocomplete is open goes to the next autocomplete option,
wrapping around at the end after reverting to the typed text first.
## Formatting
- When selecting text, a formatting bar appears above the selection.
- The formatting bar allows to format the selected test as:
- When selecting text, a formatting bar appears above the selection.
- The formatting bar allows to format the selected test as:
bold, italic, strikethrough, a block quote, and a code block (inline if no linebreak is selected).
- Formatting is applied as markdown syntax.
- Hitting ctrl/cmd+B also marks the selected text as bold
- Hitting ctrl/cmd+I also marks the selected text as italic
- Hitting ctrl/cmd+> also marks the selected text as a blockquote
- Formatting is applied as markdown syntax.
- Hitting ctrl/cmd+B also marks the selected text as bold
- Hitting ctrl/cmd+I also marks the selected text as italic
- Hitting ctrl/cmd+> also marks the selected text as a blockquote
## Misc
- When hitting the arrow-up button while having the caret at the start in the composer,
- When hitting the arrow-up button while having the caret at the start in the composer,
the last message sent by the syncing user is edited.
- Clicking a display name on an event in the timeline inserts a user pill into the composer
- Emoticons (like :-), >:-), :-/, ...) are replaced by emojis while typing if the relevant setting is enabled
- Typing in the composer sends typing notifications in the room
- Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications
- Pressing shift+enter inserts a line break
- Pressing enter sends the message.
- Choosing "Quote" in the context menu of an event inserts a quote of the event body in the composer.
- Choosing "Reply" in the context menu of an event shows a preview above the composer to reply to.
- Pressing alt+arrow up/arrow down navigates in previously sent messages, putting them in the composer.
- Clicking a display name on an event in the timeline inserts a user pill into the composer
- Emoticons (like :-), >:-), :-/, ...) are replaced by emojis while typing if the relevant setting is enabled
- Typing in the composer sends typing notifications in the room
- Pressing ctrl/mod+z and ctrl/mod+y undoes/redoes modifications
- Pressing shift+enter inserts a line break
- Pressing enter sends the message.
- Choosing "Quote" in the context menu of an event inserts a quote of the event body in the composer.
- Choosing "Reply" in the context menu of an event shows a preview above the composer to reply to.
- Pressing alt+arrow up/arrow down navigates in previously sent messages, putting them in the composer.

View File

@ -17,7 +17,7 @@ Let's say we want to close a menu when the correct keys were pressed:
```ts
const onKeyDown = (ev: KeyboardEvent): void => {
let handled = true;
const action = getKeyBindingManager().getAccessibilityAction(ev)
const action = getKeyBindingManager().getAccessibilityAction(ev);
switch (action) {
case KeyBindingAction.Escape:
closeMenu();
@ -26,12 +26,12 @@ const onKeyDown = (ev: KeyboardEvent): void => {
handled = false;
break;
}
if (handled) {
ev.preventDefault();
ev.stopPropagation();
}
}
};
```
## Managing keyboard shortcuts

View File

@ -6,6 +6,7 @@ Each .svg exports a `ReactComponent` at the named export `Icon`.
Icons have `role="presentation"` and `aria-hidden` automatically applied. These can be overriden by passing props to the icon component.
eg
```
import { Icon as FavoriteIcon } from 'res/img/element-icons/favorite.svg';

View File

@ -6,23 +6,25 @@ instructions on setting up Jitsi.
The react-sdk wraps all Jitsi call widgets in a local wrapper called `jitsi.html`
which takes several parameters:
*Query string*:
* `widgetId`: The ID of the widget. This is needed for communication back to the
react-sdk.
* `parentUrl`: The URL of the parent window. This is also needed for
communication back to the react-sdk.
_Query string_:
*Hash/fragment (formatted as a query string)*:
* `conferenceDomain`: The domain to connect Jitsi Meet to.
* `conferenceId`: The room or conference ID to connect Jitsi Meet to.
* `isAudioOnly`: Boolean for whether this is a voice-only conference. May not
be present, should default to `false`.
* `displayName`: The display name of the user viewing the widget. May not
be present or could be null.
* `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May
not be present or could be null.
* `userId`: The MXID of the user viewing the widget. May not be present or could
be null.
- `widgetId`: The ID of the widget. This is needed for communication back to the
react-sdk.
- `parentUrl`: The URL of the parent window. This is also needed for
communication back to the react-sdk.
_Hash/fragment (formatted as a query string)_:
- `conferenceDomain`: The domain to connect Jitsi Meet to.
- `conferenceId`: The room or conference ID to connect Jitsi Meet to.
- `isAudioOnly`: Boolean for whether this is a voice-only conference. May not
be present, should default to `false`.
- `displayName`: The display name of the user viewing the widget. May not
be present or could be null.
- `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May
not be present or could be null.
- `userId`: The MXID of the user viewing the widget. May not be present or could
be null.
The react-sdk will assume that `jitsi.html` is at the path of wherever it is currently
being served. For example, `https://develop.element.io/jitsi.html` or `vector://webapp/jitsi.html`.

View File

@ -10,7 +10,7 @@ implementation, such as the `RoomEchoChamber` (which handles echoable details of
Anything that can be locally echoed will be provided by the `GenericEchoChamber` implementation.
The echo chamber will also need to deal with external changes, and has full control over whether
or not something has successfully been echoed.
or not something has successfully been echoed.
An `EchoContext` is provided to echo chambers (usually with a matching type: `RoomEchoContext`
gets provided to a `RoomEchoChamber` for example) with details about their intended area of
@ -21,7 +21,7 @@ The `EchoStore` manages echo chamber instances, builds contexts, and is generall
accessible than the `EchoChamber` class. For separation of concerns, and to try and keep things
tidy, this is an intentional design decision.
**Note**: The local echo stack uses a "whenable" pattern, which is similar to thenables and
**Note**: The local echo stack uses a "whenable" pattern, which is similar to thenables and
`EventEmitter`. Whenables are ways of actioning a changing condition without having to deal
with listeners being torn down. Once the reference count of the Whenable causes garbage collection,
the Whenable's listeners will also be torn down. This is accelerated by the `IDestroyable` interface
@ -36,4 +36,3 @@ mechanisms.
The `EchoStore` is responsible for ensuring that the appropriate non-urgent toast (lower left)
is set up, where the dialog then drives through the contexts and transactions.

View File

@ -5,11 +5,12 @@ It's so complicated it needs its own README.
![](img/RoomListStore2.png)
Legend:
* Orange = External event.
* Purple = Deterministic flow.
* Green = Algorithm definition.
* Red = Exit condition/point.
* Blue = Process definition.
- Orange = External event.
- Purple = Deterministic flow.
- Green = Algorithm definition.
- Red = Exit condition/point.
- Blue = Process definition.
## Algorithms involved
@ -22,7 +23,6 @@ Behaviour of the overall room list (sticky rooms, etc) are determined by the gen
class. Here is where much of the coordination from the room list store is done to figure out which list
algorithm to call, instead of having all the logic in the room list store itself.
Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm
the power to decide when and how to apply the tag sorting, if at all. For example, the importance algorithm,
later described in this document, heavily uses the list ordering behaviour to break the tag into categories.
@ -68,14 +68,14 @@ simply get the manual sorting algorithm applied to them with no further involvem
algorithm. There are 4 categories: Red, Grey, Bold, and Idle. Each has their own definition based off
relative (perceived) importance to the user:
* **Red**: The room has unread mentions waiting for the user.
* **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread
messages which cause a push notification or badge count. Typically, this is the default as rooms get
set to 'All Messages'.
* **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without
a badge/notification count (or 'Mentions Only'/'Muted').
* **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user
last read it.
- **Red**: The room has unread mentions waiting for the user.
- **Grey**: The room has unread notifications waiting for the user. Notifications are simply unread
messages which cause a push notification or badge count. Typically, this is the default as rooms get
set to 'All Messages'.
- **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without
a badge/notification count (or 'Mentions Only'/'Muted').
- **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user
last read it.
Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey
above bold, etc.

View File

@ -8,7 +8,6 @@ During an onscroll event, we check whether we're getting close to the top or bot
ScrollPanel supports a mode to prevent it shrinking. This is used to prevent a jump when at the bottom of the timeline and people start and stop typing. It gets cleared automatically when 200px above the bottom of the timeline.
## BACAT (Bottom-Aligned, Clipped-At-Top) scrolling
BACAT scrolling implements a different way of restoring the scroll position in the timeline while tiles out of view are changing height or tiles are being added or removed. It was added in https://github.com/matrix-org/matrix-react-sdk/pull/2842.

View File

@ -5,23 +5,22 @@ different values for a setting at particular levels of interest. For example, a
they want URL previews off, but in all other rooms they want them enabled. The `SettingsStore` helps mask the complexity
of dealing with the different levels and exposes easy to use getters and setters.
## Levels
Granular Settings rely on a series of known levels in order to use the correct value for the scenario. These levels, in
order of priority, are:
* `device` - The current user's device
* `room-device` - The current user's device, but only when in a specific room
* `room-account` - The current user's account, but only when in a specific room
* `account` - The current user's account
* `room` - A specific room (setting for all members of the room)
* `config` - Values are defined by the `setting_defaults` key (usually) in `config.json`
* `default` - The hardcoded default for the settings
- `device` - The current user's device
- `room-device` - The current user's device, but only when in a specific room
- `room-account` - The current user's account, but only when in a specific room
- `account` - The current user's account
- `room` - A specific room (setting for all members of the room)
- `config` - Values are defined by the `setting_defaults` key (usually) in `config.json`
- `default` - The hardcoded default for the settings
Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure
that room administrators cannot force account-only settings upon participants.
## Settings
Settings are the different options a user may set or experience in the application. These are pre-defined in
@ -29,6 +28,7 @@ Settings are the different options a user may set or experience in the applicati
Settings that support the config level can be set in the config file under the `setting_defaults` key (note that some
settings, like the "theme" setting, are special cased in the config file):
```json5
{
...
@ -56,13 +56,14 @@ target level.
Values are defined at particular levels and should be done in a safe manner. There are two checks to perform to ensure a
clean save: is the level supported and can the user actually set the value. In most cases, neither should be an issue
although there are circumstances where this changes. An example of a safe call is:
```javascript
const isSupported = SettingsStore.isLevelSupported(SettingLevel.ROOM);
if (isSupported) {
const canSetValue = SettingsStore.canSetValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM);
if (canSetValue) {
SettingsStore.setValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM, newValue);
}
const canSetValue = SettingsStore.canSetValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM);
if (canSetValue) {
SettingsStore.setValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM, newValue);
}
}
```
@ -73,19 +74,14 @@ instance, the component which allows changing the setting may be hidden conditio
Where possible, the `SettingsFlag` component should be used to set simple "flip-a-bit" (true/false) settings. The
`SettingsFlag` also supports simple radio button options, such as the theme the user would like to use.
```html
<SettingsFlag name="theSettingId"
level={SettingsLevel.ROOM}
roomId="!curbf:matrix.org"
label={_td("Your label here")} // optional, if falsey then the `SettingsStore` will be used
onChange={function(newValue) { }} // optional, called after saving
isExplicit={false} // this is passed along to `SettingsStore.getValueAt`, defaulting to false
manualSave={false} // if true, saving is delayed. You will need to call .save() on this component
// Options for radio buttons
group="your-radio-group" // this enables radio button support
value="yourValueHere" // the value for this particular option
/>
```html
<SettingsFlag name="theSettingId" level={SettingsLevel.ROOM} roomId="!curbf:matrix.org" label={_td("Your label here")}
// optional, if falsey then the `SettingsStore` will be used onChange={function(newValue) { }} // optional, called after
saving isExplicit={false} // this is passed along to `SettingsStore.getValueAt`, defaulting to false manualSave={false}
// if true, saving is delayed. You will need to call .save() on this component // Options for radio buttons
group="your-radio-group" // this enables radio button support value="yourValueHere" // the value for this particular
option />
```
### Getting the display name for a setting
@ -93,16 +89,16 @@ Where possible, the `SettingsFlag` component should be used to set simple "flip-
Simply call `SettingsStore.getDisplayName`. The appropriate display name will be returned and automatically translated
for you. If a display name cannot be found, it will return `null`.
## Features
Feature flags are just like regular settings with some underlying semantics for how they are meant to be used. Usually
a feature flag is used when a portion of the application is under development or not ready for full release yet, such
as new functionality or experimental ideas. In these cases, the feature name *should* be named with the `feature_*`
as new functionality or experimental ideas. In these cases, the feature name _should_ be named with the `feature_*`
convention and must be tagged with `isFeature: true` in the setting definition. By doing so, the feature will automatically
appear in the "labs" section of the user's settings.
Features can be controlled at the config level using the following structure:
```json
"features": {
"feature_lazyloading": true
@ -144,7 +140,6 @@ additional steps to actually enable notifications.
For more information, see `src/settings/controllers/SettingController.ts`.
## Local echo
`SettingsStore` will perform local echo on all settings to ensure that immediately getting values does not cause a
@ -160,7 +155,6 @@ SettingsStore.setValue(...).then(() => {
SettingsStore.getValue(...); // this will return the value set in `setValue` above.
```
## Watching for changes
Most use cases do not need to set up a watcher because they are able to react to changes as they are made, or the
@ -174,12 +168,11 @@ An example of a watcher in action would be:
```javascript
class MyComponent extends React.Component {
settingWatcherRef = null;
componentWillMount() {
const callback = (settingName, roomId, level, newValAtLevel, newVal) => {
this.setState({color: newVal});
this.setState({ color: newVal });
};
this.settingWatcherRef = SettingsStore.watchSetting("roomColor", "!example:matrix.org", callback);
}
@ -190,7 +183,6 @@ class MyComponent extends React.Component {
}
```
# Maintainers Reference
The granular settings system has a few complex parts to power it. This section is to document how the `SettingsStore` is

View File

@ -13,12 +13,12 @@ It exposes a function over a postMessage API, when sent an object with the match
```json5
{
"imgSrc": "", // the src of the image to display in the download link
"imgStyle": "", // the style to apply to the image
"style": "", // the style to apply to the download link
"download": "", // download attribute to pass to the <a/> tag
"textContent": "", // the text to put inside the download link
"blob": "", // the data blob to wrap in an object url and allow the user to download
imgSrc: "", // the src of the image to display in the download link
imgStyle: "", // the style to apply to the image
style: "", // the style to apply to the download link
download: "", // download attribute to pass to the <a/> tag
textContent: "", // the text to put inside the download link
blob: "", // the data blob to wrap in an object url and allow the user to download
}
```

View File

@ -4,24 +4,25 @@ Rooms can have a default widget layout to auto-pin certain widgets, make the con
sizes, etc. These are defined through the `io.element.widgets.layout` state event (empty state key).
Full example content:
```json5
{
"widgets": {
"first-widget-id": {
"container": "top",
"index": 0,
"width": 60,
"height": 40
widgets: {
"first-widget-id": {
container: "top",
index: 0,
width: 60,
height: 40,
},
"second-widget-id": {
container: "right",
},
},
"second-widget-id": {
"container": "right"
}
}
}
```
As shown, there are two containers possible for widgets. These containers have different behaviour
and interpret the other options differently.
and interpret the other options differently.
## `top` container
@ -32,7 +33,7 @@ therefore fewer messages can be shown).
The `index` for a widget determines which order the widgets show up in from left to right. Widgets
without an `index` will show up as the rightmost widgets. Tiebreaks (same `index` or multiple defined
without an `index`) are resolved by comparing widget IDs. A maximum of 3 widgets can be in the top
container - any which exceed this will be ignored (placed into the `right` container). Smaller numbers
container - any which exceed this will be ignored (placed into the `right` container). Smaller numbers
represent leftmost widgets.
The `width` is relative width within the container in percentage points. This will be clamped to a
@ -43,7 +44,7 @@ attempt to show them at 33% width each.
Note that the client may impose minimum widths on the widgets, such as a 10% minimum to avoid pinning
hidden widgets. In general, widgets defined in the 30-70% range each will be free of these restrictions.
The `height` is not in fact applied per-widget but is recorded per-widget for potential future
The `height` is not in fact applied per-widget but is recorded per-widget for potential future
capabilities in future containers. The top container will take the tallest `height` and use that for
the height of the whole container, and thus all widgets in that container. The `height` is relative
to the container, like with `width`, meaning that 100% will consume as much space as the client is